Julia 朱莉娅-国王之路(发电机性能)
我有一些python代码,我试图移植到Julia来学习这门可爱的语言。我在python中使用了生成器。在移植之后,我觉得朱莉娅在这方面真的很慢 我将部分代码简化为以下练习: 想想4x4棋盘吧。找到每一个N步长的路径,象棋王可以做到。在这个练习中,国王不允许在同一条路径上的同一位置跳跃两次。不要浪费内存->创建每条路径的生成器 算法非常简单: 如果我们在每个位置都用数字签名:Julia 朱莉娅-国王之路(发电机性能),julia,Julia,我有一些python代码,我试图移植到Julia来学习这门可爱的语言。我在python中使用了生成器。在移植之后,我觉得朱莉娅在这方面真的很慢 我将部分代码简化为以下练习: 想想4x4棋盘吧。找到每一个N步长的路径,象棋王可以做到。在这个练习中,国王不允许在同一条路径上的同一位置跳跃两次。不要浪费内存->创建每条路径的生成器 算法非常简单: 如果我们在每个位置都用数字签名: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 16 点0有3个邻居(1、4、5)
0 1 2 3
4 5 6 7
8 9 10 11
12 13 14 16
点0有3个邻居(1、4、5)。我们可以为每个点的每个邻居找到一张表:
[1、3、5、6、7、(2、5、6、7)、2、6、7、2、6、6、7、2、6、6、7、6、7、6、7、7、7、7、5、5、5、5、8、8、8、8、9、9、0、0、0、1、1、1、1、1、1、4、3、3、3、3、5、3、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、5、]]
PYTHON
一个递归函数(生成器),用于从点列表或点生成器(生成器…)放大给定路径:
def放大(路径):
如果isinstance(路径,列表):
对于NEIG[path[-1]]中的i:
如果我不在路径中:
屈服路径[:]+[i]
其他:
对于路径中的i:
放大收益率(一)
函数(生成器),它提供具有给定长度的每条路径
def路径(长度):
步骤=([i]表示范围(16)内的i)#板上每个点的第一步
对于范围内的(长度-1):
nsteps=放大(步数)
步骤=n步骤
步履蹒跚
我们可以看到长度为10的路径有905776条:
sum(路径(10)中的i为1)
Out[89]:905776
朱莉娅
(由@gggg在我们的讨论中创建)
基准
在ipython,我们可以计时:
python 3.6.3:
朱莉娅0.6.0
julia> @time sum(1 for path in paths(10))
2.690630 seconds (41.91 M allocations: 1.635 GiB, 11.39% gc time)
905776
Julia 0.7.0-DEV.0
julia> @time sum(1 for path in paths(10))
4.951745 seconds (35.69 M allocations: 1.504 GiB, 4.31% gc time)
905776
问题:
我们朱利安是:需要注意的是,编写基准代码并不是为了获得绝对的最大性能(计算递归的最快代码是常量文字6765)。相反,编写基准测试是为了测试用每种语言实现的相同算法和代码模式的性能
在这个基准测试中,我们使用了相同的想法。仅适用于封闭在发电机上的阵列上的循环。(numpy、numba、pandas或其他c编写和编译的python包中没有任何内容)
假设朱莉娅的发电机非常慢,对吗
我们能做些什么使它真正快速 没有遵循相同的算法(也不知道Python这样做的速度有多快),但是对于长度为10的解决方案,Julia基本上是相同的,对于长度为16的解决方案,Julia要好得多
In [48]: %timeit sum(1 for path in paths(10))
1.52 s ± 11.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
julia> @time sum(1 for path in pathsr(10))
1.566964 seconds (5.54 M allocations: 693.729 MiB, 16.24% gc time)
905776
In [49]: %timeit sum(1 for path in paths(16))
19.3 s ± 15.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
julia> @time sum(1 for path in pathsr(16))
6.491803 seconds (57.36 M allocations: 9.734 GiB, 33.79% gc time)
343184
这是代码。我昨天刚刚了解了任务/渠道,因此可能可以做得更好:
const NEIG = [[1, 4, 5], [0, 2, 4, 5, 6], [1, 3, 5, 6, 7], [2, 6, 7], [0, 1, 5, 8, 9], [0, 1, 2, 4, 6, 8, 9, 10], [1, 2, 3, 5, 7, 9, 10, 11], [2, 3, 6, 10, 11], [4, 5, 9, 12, 13], [4, 5, 6, 8, 10, 12, 13, 14], \
[5, 6, 7, 9, 11, 13, 14, 15], [6, 7, 10, 14, 15], [8, 9, 13], [8, 9, 10, 12, 14], [9, 10, 11, 13, 15], [10, 11, 14]];
function enlarger(num::Int,len::Int,pos::Int,sol::Array{Int64,1},c::Channel)
if pos == len
put!(c,copy(sol))
elseif pos == 0
for j=0:num
sol[1]=j
enlarger(num,len,pos+1,sol,c)
end
close(c)
else
for i in NEIG[sol[pos]+1]
if !in(i,sol[1:pos])
sol[pos+1]=i
enlarger(num,len,pos+1,sol,c)
end
end
end
end
function pathsr(len)
c=Channel(0)
sol = [0 for i=1:len]
@schedule enlarger(15,len,0,sol,c)
(i for i in c)
end
基准测试(JIT编译执行一次后):
这要快得多。茱莉亚的“比Python更好的性能”并不神奇。它的大部分直接来源于这样一个事实:Julia可以找出函数中每个变量的类型,然后为这些特定类型编译高度专业化的代码。这甚至适用于许多容器和容器中的元素,如发电机;朱莉娅通常提前知道元素的类型。Python几乎不能轻松地进行这种分析(或者在许多情况下根本不能),因此它的优化集中在改进动态行为上
为了让Julia的生成器提前知道它们可能生成的类型,它们封装了有关它们执行的操作以及它们在类型中迭代的对象的信息:
julia> (1 for i in 1:16)
Base.Generator{UnitRange{Int64},getfield(Main, Symbol("##27#28"))}(getfield(Main, Symbol("##27#28"))(), 1:16)
奇怪的#27#28
是一种只返回1
的匿名函数。当生成器到达LLVM时,它已经知道了足够的信息来执行大量优化:
julia> function naive_sum(c)
s = 0
for elt in c
s += elt
end
s
end
@code_llvm naive_sum(1 for i in 1:16)
; Function naive_sum
; Location: REPL[1]:2
define i64 @julia_naive_sum_62385({ { i64, i64 } } addrspace(11)* nocapture nonnull readonly dereferenceable(16)) {
top:
; Location: REPL[1]:3
%1 = getelementptr inbounds { { i64, i64 } }, { { i64, i64 } } addrspace(11)* %0, i64 0, i32 0, i32 0
%2 = load i64, i64 addrspace(11)* %1, align 8
%3 = getelementptr inbounds { { i64, i64 } }, { { i64, i64 } } addrspace(11)* %0, i64 0, i32 0, i32 1
%4 = load i64, i64 addrspace(11)* %3, align 8
%5 = add i64 %4, 1
%6 = sub i64 %5, %2
; Location: REPL[1]:6
ret i64 %6
}
在那里解析LLVM IR可能需要一分钟,但您应该能够看到它只是提取UnitRange
(getelementptr
和load
)的端点,将它们彼此相减(sub
),然后添加一个端点来计算总和,而不需要单个循环
不过,在本例中,它对Julia起作用:path(10)
的类型复杂得可笑!您正在迭代地将一个生成器包装在过滤器中,并将其展平,还有更多的生成器。事实上,它变得如此复杂,以至于茱莉亚放弃了尝试去理解它,决定生活在动态行为中。在这一点上,它不再具有与Python相比的固有优势——事实上,在递归遍历对象时专门处理如此多的不同类型将是一个明显的障碍。通过查看@code\u warntype start(1表示路径(10)中的i))
可以看到这一点
我对Julia性能的经验法则是,代码通常在C的2倍以内,而动态、不稳定或矢量化的代码在Python/MATLAB/其他高级语言的数量级以内。通常它会稍微慢一点,因为其他高级语言非常努力地优化它们的案例,而Julia的大多数优化都集中在类型稳定的方面。这种深度嵌套的结构将您置于动态阵营中
朱莉娅的发电机是不是非常慢?并非天生如此;正是当它们如此深入地嵌套在一起时,你才遇到了这种糟糕的情况。遵循Thoy的答案,因为元组似乎非常快。这与我之前的代码类似,但是使用了元组的东西,它得到了更好的结果:
julia> @time sum(1 for i in pathst(10))
1.155639 seconds (1.83 M allocations: 97.632 MiB, 0.75% gc time)
905776
julia> @time sum(1 for i in pathst(16))
1.963470 seconds (1.39 M allocations: 147.555 MiB, 0.35% gc time)
343184
Th
julia> @time npaths(10)
0.069531 seconds (5 allocations: 176 bytes)
905776
julia> (1 for i in 1:16)
Base.Generator{UnitRange{Int64},getfield(Main, Symbol("##27#28"))}(getfield(Main, Symbol("##27#28"))(), 1:16)
julia> function naive_sum(c)
s = 0
for elt in c
s += elt
end
s
end
@code_llvm naive_sum(1 for i in 1:16)
; Function naive_sum
; Location: REPL[1]:2
define i64 @julia_naive_sum_62385({ { i64, i64 } } addrspace(11)* nocapture nonnull readonly dereferenceable(16)) {
top:
; Location: REPL[1]:3
%1 = getelementptr inbounds { { i64, i64 } }, { { i64, i64 } } addrspace(11)* %0, i64 0, i32 0, i32 0
%2 = load i64, i64 addrspace(11)* %1, align 8
%3 = getelementptr inbounds { { i64, i64 } }, { { i64, i64 } } addrspace(11)* %0, i64 0, i32 0, i32 1
%4 = load i64, i64 addrspace(11)* %3, align 8
%5 = add i64 %4, 1
%6 = sub i64 %5, %2
; Location: REPL[1]:6
ret i64 %6
}
julia> @time sum(1 for i in pathst(10))
1.155639 seconds (1.83 M allocations: 97.632 MiB, 0.75% gc time)
905776
julia> @time sum(1 for i in pathst(16))
1.963470 seconds (1.39 M allocations: 147.555 MiB, 0.35% gc time)
343184
const NEIG = [[1, 4, 5], [0, 2, 4, 5, 6], [1, 3, 5, 6, 7], [2, 6, 7], [0, 1, 5, 8, 9], [0, 1, 2, 4, 6, 8, 9, 10], [1, 2, 3, 5, 7, 9, 10, 11], [2, 3, 6, 10, 11], [4, 5, 9, 12, 13], [4, 5, 6, 8, 10, 12, 13, 14], [5, 6, 7, 9, 11, 13, 14, 15], [6, 7, 10, 14, 15], [8, 9, 13], [8, 9, 10, 12, 14], [9, 10, 11, 13, 15], [10, 11, 14]];
function enlarget(path,len,c::Channel)
if length(path) >= len
put!(c,path)
else
for loc in NEIG[path[end]+1]
loc in path && continue
enlarget((path..., loc), len,c)
end
if length(path) == 1
path[1] == 15 ? close(c) : enlarget((path[1]+1,),len,c)
end
end
end
function pathst(len)
c=Channel(0)
path=(0,)
@schedule enlarget(path,len,c)
(i for i in c)
end
const NEIG_py = [[1, 4, 5], [0, 2, 4, 5, 6], [1, 3, 5, 6, 7], [2, 6, 7], [0, 1, 5, 8, 9], [0, 1, 2, 4, 6, 8, 9, 10], [1, 2, 3, 5, 7, 9, 10, 11], [2, 3, 6, 10, 11], [4, 5, 9, 12, 13], [4, 5, 6, 8, 10, 12, 13, 14], [5, 6, 7, 9, 11, 13, 14, 15], [6, 7, 10, 14, 15], [8, 9, 13], [8, 9, 10, 12, 14], [9, 10, 11, 13, 15], [10, 11, 14]];
const NEIG = [n.+1 for n in NEIG_py]
function enlargetc(path,len,c::Function)
if length(path) >= len
c(path)
else
for loc in NEIG[path[end]]
loc in path && continue
enlargetc((path..., loc), len,c)
end
if length(path) == 1
if path[1] == 16 return
else enlargetc((path[1]+1,),len,c)
end
end
end
end
function get_counter()
let helper = 0
function f(a)
helper += 1
return helper
end
return f
end
end
counter = get_counter()
@time enlargetc((1,), 10, counter) # 0.481986 seconds (2.62 M allocations: 154.576 MiB, 5.12% gc time)
counter.helper.contents # 905776
import Base.Iterators: start, next, done, eltype, iteratoreltype, iteratorsize
struct SAWsIterator
neigh::Vector{Vector{Int}}
pathlen::Int
pos::Int
end
SAWs(neigh, pathlen, pos) = SAWsIterator(neigh, pathlen, pos)
start(itr::SAWsIterator) =
([itr.pos ; zeros(Int, itr.pathlen-1)], Vector{Int}(itr.pathlen-1),
2, Ref{Bool}(false), Ref{Bool}(false))
@inline next(itr::SAWsIterator, s) =
( s[4][] ? s[4][] = false : calc_next!(itr, s) ;
(s[1], (s[1], s[2], itr.pathlen, s[4], s[5])) )
@inline done(itr::SAWsIterator, s) = ( s[4][] || calc_next!(itr, s) ; s[5][] )
function calc_next!(itr::SAWsIterator, s)
s[4][] = true ; s[5][] = false
curindex = s[3]
pathlength = itr.pathlen
path, options = s[1], s[2]
@inbounds while curindex<=pathlength
curindex == 1 && ( s[5][] = true ; break )
startindex = path[curindex] == 0 ? 1 : options[curindex-1]+1
path[curindex] = 0
i = findnext(x->!(x in path), neigh[path[curindex-1]], startindex)
if i==0
path[curindex] = 0 ; options[curindex-1] = 0 ; curindex -= 1
else
path[curindex] = neigh[path[curindex-1]][i]
options[curindex-1] = i ; curindex += 1
end
end
return nothing
end
eltype(::Type{SAWsIterator}) = Vector{Int}
iteratoreltype(::Type{SAWsIterator}) = Base.HasEltype()
iteratorsize(::Type{SAWsIterator}) = Base.SizeUnknown()
allSAWs(neigh, pathlen) =
Base.Flatten(SAWs(neigh,pathlen,k) for k in eachindex(neigh))
iterlength(itr) = mapfoldl(x->1, +, 0, itr)
using Base.Test
const neigh = [[2, 5, 6], [1, 3, 5, 6, 7], [2, 4, 6, 7, 8], [3, 7, 8],
[1, 2, 6, 9, 10], [1, 2, 3, 5, 7, 9, 10, 11], [2, 3, 4, 6, 8, 10, 11, 12],
[3, 4, 7, 11, 12], [5, 6, 10, 13, 14], [5, 6, 7, 9, 11, 13, 14, 15],
[6, 7, 8, 10, 12, 14, 15, 16], [7, 8, 11, 15, 16], [9, 10, 14],
[9, 10, 11, 13, 15], [10, 11, 12, 14, 16], [11, 12, 15]]
@test iterlength(allSAWs(neigh, 10)) == 905776
for (i,path) in enumerate(allSAWs(neigh, 10))
if i % 100_000 == 0
@show i,path
end
end
@time iterlength(allSAWs(neigh, 10))
(i, path) = (100000, [2, 5, 10, 14, 9, 6, 7, 12, 15, 11])
(i, path) = (200000, [4, 3, 8, 7, 6, 10, 14, 11, 16, 15])
(i, path) = (300000, [5, 10, 11, 16, 15, 14, 9, 6, 7, 3])
(i, path) = (400000, [8, 3, 6, 5, 2, 7, 11, 14, 15, 10])
(i, path) = (500000, [9, 14, 10, 5, 2, 3, 8, 11, 6, 7])
(i, path) = (600000, [11, 16, 15, 14, 10, 6, 3, 8, 7, 12])
(i, path) = (700000, [13, 10, 15, 16, 11, 6, 2, 1, 5, 9])
(i, path) = (800000, [15, 11, 12, 7, 2, 3, 6, 1, 5, 9])
(i, path) = (900000, [16, 15, 14, 9, 5, 10, 7, 8, 12, 11])
0.130755 seconds (4.16 M allocations: 104.947 MiB, 11.37% gc time)
905776