Julia

时间:2017-08-08 06:23:51

标签: performance random julia

我有一个简单的函数,它出现在我的Julia代码中的几个地方,并且在循环中运行了数百万次。该函数基本上是rand([1,-1,im,-im]),即它选择四个可能的给定值之一。我注意到这个函数在我的庞大循环中占用了大量的时间,因此,我尝试以稍微快一点的方式编写它:

function qpsk()
  temp1 = ifelse(rand(Bool), 1+0im, -1+0im)
  temp2 = ifelse(rand(Bool), 1+0im,  0+1im)
  temp1*temp2
end

然后,它通常被称为:

sig = complex(zeros(N))
for i = 1:N
  sig[i] = qpsk()
end

现在,有没有办法进一步优化此功能,或使用其他更快的方法?感谢您的帮助。

对当前答案的评论:

@DanGetz(22行??)的答案并没有解决问题,因为目前,Julia并不像使用显式循环那样善于向量。也, 我的简单,下面的1行qpsk2(s)比Dan的原始答案中的那些“神秘的”22行代码快2倍(创建了一个矢量,这增加了更多的时间)。

但问题仍然存在,为什么呢 没有实现下面的qpsk1之类的东西?为什么我的原始qpsk分支比下面的简单qpsk4(s)快3倍?

如果有经验的人喜欢参加,我在下面添加了更多版本来指导讨论。

qpsk1(s) = s[1+(rand(Int8)&3)]          # Blazingly fast
qpsk2(s) = s[1+rand(Bool)+2rand(Bool)]  # Very fast
qpsk3(s) = s[rand(1:4,1)]               # Compiler issue here?
qpsk4(s) = s[rand(1:4)]                 # Why slow?
qpsk5(s) = rand([s])                    # Ridiculously slow!!
function test_orig(n)                   # Test qpsk(), very fast(branching!), why?
  for i = 1:n
    qpsk()
  end
end

using StaticArrays
function test(func, n)                  # Test all qpsk1 --> qpsk5
  s = SVector(1,-1,im,-im)
  for i=1:n
    func(s)
  end
end

@time test(qpsk1,10^8)  0.554994 seconds (5 allocations: 176 bytes)
@time test(qpsk2,10^8)  0.755286 seconds (5 allocations: 176 bytes)
@time test(qpsk3,10^8) 13.431529 seconds (400 M allocations: 26.822 GiB, 20.68% gc time)
@time test(qpsk4,10^8)  2.520085 seconds (5 allocations: 176 bytes)
@time test(qpsk5,10^8) 10.881852 seconds (200 M allocations: 20.862 GiB, 19.76% gc time)
@time test_orig(10^8)   0.771778 seconds (5 allocations: 176 bytes)
@time nqpsk2(10^8);     1.402830 seconds (9 allocations: 1.490 GiB, 6.39% gc time)

2 个答案:

答案 0 :(得分:6)

答案摘要

[(-1)^b1*im^b2 for (b1,b2) in zip(rand!(BitVector(N)),rand!(BitVector(N)))]

更快地生成长度为N的矢量。

<强>答案

计算随机位是工作的主要部分,因此从使用RandomNumbers.jl的注释中探索Chris的想法值得一试。另外,我们可以使用@ rickhg12hs的想法从生成的每个随机数中提取更多位。无论如何,一起生成一个值块对于更好的优化至关重要。

例如,以下代码(nqpsk1使用问题中的qpsk作为基线。nqpsk2是建议的改进措施:

function qpsk()
  temp1 = ifelse(rand(Bool), 1+0im, -1+0im)
  temp2 = ifelse(rand(Bool), 1+0im,  0+1im)
  temp1*temp2
end

nqpsk1(n::Int) = [qpsk() for i=1:n]

nqpsk2(n::Int) = begin
    res = zeros(Int,2*n)
    blocks = n >>> 4                 # use blocks of 16 values
    btail = n & 0x000000000000000f   # in case n is not a multiple of 16
    pos = 1
    @inbounds for i=1:blocks
        bits = rand(UInt32)          # get random bits for a whole block
        for j=1:16
            b1 = Bool(bits & 1)
            bits >>>= 1
            b2 = Bool(bits & 1)
            bits >>>= 1
            res[pos+b1] = (-1)^b2
            pos += 2
        end
    end
    @inbounds for i=1:btail
        res[pos+rand(Bool)] = (-1)^rand(Bool)
        pos += 2
    end
    return reinterpret(Complex{Int64},res)
end

在我的设置上实现了> 4倍的改进(Julia 0.7):

julia> using BenchmarkTools

julia> @btime nqpsk1(320);
  8.791 μs (323 allocations: 15.19 KiB)

julia> @btime nqpsk2(320);
  1.056 μs (3 allocations: 5.20 KiB)

<强>更新

速度(和一些分配)只有适度的折衷,但更好看的代码:

function nqpsk3(n::Int)
    res = zeros(Int,2n)
    rv1 = rand!(BitVector(n))
    rv2 = rand!(BitVector(n))
    @inbounds for (b1,b2,i) in zip(rv1,rv2,1:2:2n)
        res[i+b1] = (-1)^b2
    end
    return reinterpret(Complex{Int},res)
end

基准:

julia> @btime nqpsk3(320);
  1.780 μs (11 allocations: 5.83 KiB)

<强>附录

单一(包裹)线版本也可以(2.48μs):

nqpsk4(n) = [(1+0im,-1+0im,0+im,0-im)[2b1+b2+1] for
  (b1,b2) in zip(rand!(BitVector(n)),rand!(BitVector(n)))]

最后,真正的单行版本(1.96μs):

nqpsk5(n) = [(-1)^b1*im^b2 for (b1,b2) in zip(rand!(BitVector(n)),rand!(BitVector(n)))]

答案 1 :(得分:-3)

最新调查状况

我目前最好的解决方案如下:

function g(pX::Array{Complex{Float64},1})
    tab = [1.0,im,-1.0,-im]
    bits = UInt128(0)
    @inbounds for i = 1 : length(pX)
        bits = (i % 64) == 1 ? rand(UInt128) : bits >>> 2
        pX[i] = tab[(bits & 3)+1]
    end
end

sig = complex(zeros(1280));
using BenchmarkTools
@btime g(sig)

3.838 μs (13 allocations: 464 bytes)

这比我使用相同N运行的优化版Dan Getz更好,我感觉更具可读性

4.236 μs (4 allocations: 20.16 KiB)

然而,表现极其脆弱。只是看看 36倍慢版本的微妙差异:

function g(pX::Array{Complex{Float64},1})
    tab = [1,im,-1,-im]
    bits = 0
    for i = 1 : length(pX)
        bits = (i % 64) == 1 ? rand(UInt128) : bits >>> 2
        pX[i] = tab[(bits & 3)+1]
    end
end

138.320 μs (10209 allocations: 319.14 KiB)

你找到了差异吗?

  • 没有从Int64转换为Float64
  • 类型稳定性
  • 禁用范围检查

遵循惯例g()应该重命名为g!()

在下文中,您可以找到当前最佳定时解决方案的演变

我的第一个回答方法是解决一般性弱点

a)由于调用开销,调用函数很昂贵。

b)复杂计算比查找更耗时。

这最终得到了提案

 cases = [1+0im,0+1im,-1+0im,0-1im]
 g() = cases[rand(1:4)]
 // to use just call g() 
 g()

发生了什么?

为什么a)没有成功?

using BenchmarkTools
test(n) = [q() for i = 1:n]

g() = rand()
@btime test(800);

结果

  • rand()=&gt; 5.784
  • rand(Float32)=&gt; 5.604
  • rand(Float64)=&gt; 5.821

  • rand(Bool)=&gt; 5.167

  • rand(Int8)=&gt; 5.126
  • rand(Int16)=&gt; 5.171

  • rand(Int32)=&gt; 5.631

  • rand(Int64)=&gt; 7.980
  • rand(Int128)=&gt; 10.549

  • rand(1:4)=&gt; 28.603

  • (rand(Int8)%4)+ 1 =&gt; 6.053
  • (rand(Int8)&amp; 3)+ 1 =&gt; 5.843

  • rand(0:255)=&gt; 28.568

  • rand(UInt8)=&gt; 5.104

  • rand([1,2,3,4])=&gt; 58.437

  • l = [1,2,3,4]; g()= rand(l)=&gt; 47.399
  • rand(l,1)=&gt; 70.052

  • m =(1,2,3,4); rand(m)=&gt; 124.311

  • 0 =&gt; 0.872

  • 0.0 =&gt; 0.887
  • Int8(0)=&gt; 0.113
  • return =&gt; 0.33

(在Ubuntu上运行Julia 0.6)

如何判断结果

请求float32和float64需要同样的时间。这可能是一个指示,即float64不是完整的mantisse(56位)随机值

对于Bool,Int8,Int16的rand需要几乎相同的时间。可能只是使用较少的位来使用相同的算法。

对于Int32的rand稍微耗费时间。 Int64和Int128占用的时间比例更长。

兰德(1:4)花了更多的时间。它应该在rand(Int8)的范围内,因为它等于(rand(Int8)%4)+ 1和(rand(Int8)&amp; 3)+ 1。 即使我伤害了某些人的宗教感情,这只是糟糕的代码。

与rand(Uint)和rand(0:255)相同

rand with array和tupel的性能远远不能接受!

为什么b)没有成功?

Julia似乎无法从tupels或数组中有效查找。 但即使查找速度很快,rand方法也占主导地位。

其他方法

Dan Getz方法使用来自rand调用的所有位。所以最终它需要在他的第一个算法中每个值调用1/16。

但是,使用UInt128可以改善这种方法,因为现在每个值需要1/64次调用。

在我的机器上,Dan Getz原始代码的1280值为17.314,修改后的代码为4.595。这种改进与减少rand数量的比例成正比!

test2(n::Int) = begin
    res = zeros(Int,2*n)
    blocks = n >>> 7                 # use blocks of 16 values
    btail = n & 0x000000000000007f   # in case n is not a multiple of 16
    pos = 1
    @inbounds for i=1:blocks
        bits = rand(UInt128)          # get random bits for a whole block
        for j=1:16
            b1 = Bool(bits & 1)
            bits >>>= 1
            b2 = Bool(bits & 1)
            bits >>>= 1
            res[pos+b1] = (-1)^b2
            pos += 2
        end
    end
    @inbounds for i=1:btail
        res[pos+rand(Bool)] = (-1)^rand(Bool)
        pos += 2
    end
    return reinterpret(Complex{Int64},res)
end

@btime test2(1280);

然而,使用重新解释意味着知道不同结构的位布局。这不是一个好主意。

高级视图

最后,所有被提问者编码的是一个复杂的复杂构建的随机数组,从1到4(或0到3)。我会尝试优化跟随任务的提问者的下一步。但是,没有提供任何信息。

在下面的案例中,朱莉娅的表现要好得多,听起来有些奇怪。更多的回报,更少的时间??

@btime rand(0:3, 1280)
=> 24.377

PS: 仅仅为了将数字与Dan Getz最后一种方法进行比较,以下代码需要27.004

N=1280
@btime [(-1)^b1*im^b2 for (b1,b2) in zip(rand!(BitVector(N)),rand!(BitVector(N)))]