我们经常需要处理由坐标列表组成的数据:data = {{x1,y1}, {x2,y2}, ..., {xn,yn}}
。它可以是2D或3D坐标,也可以是固定长度小矢量的任意其他任意长度列表。
让我举例说明如何使用Compile
来解决这些问题,使用简单的总结2D矢量列表的例子:
data = RandomReal[1, {1000000, 2}];
首先,显而易见的版本:
fun1 = Compile[{{vec, _Real, 2}},
Module[{sum = vec[[1]]},
Do[sum += vec[[i]], {i, 2, Length[vec]}];
sum
]
]
它有多快?
In[13]:= Do[fun1[data], {10}] // Timing
Out[13]= {4.812, Null}
第二,不太明显的版本:
fun2 = Compile[{{vec, _Real, 1}},
Module[{sum = vec[[1]]},
Do[sum += vec[[i]], {i, 2, Length[vec]}];
sum
]
]
In[18]:= Do[
fun2 /@ Transpose[data],
{10}
] // Timing
Out[18]= {1.078, Null}
如您所见,第二个版本要快得多。为什么?由于关键操作sum += ...
是fun2
中的数字添加,而fun1
中添加了任意长度向量。
您可以看到相同“优化”in this asnwer of mine的实际应用,但在相关的情况下可以给出许多其他示例。
现在在这个简单的示例中,使用fun2
的代码不比fun1
更长或更复杂,但在一般情况下,它可能很好。
如何判断Compile
其中一个参数不是任意n*m
矩阵,而是一个特殊的n*2
或n*3
矩阵,因此它可以自动进行这些优化,而不是使用通用向量加法函数来添加微小的长度为2或长度为3的向量?
为了更清楚地了解发生了什么,我们可以使用CompilePrint
:
CompilePrint[fun1]
给出了
1 argument
5 Integer registers
5 Tensor registers
Underflow checking off
Overflow checking off
Integer overflow checking on
RuntimeAttributes -> {}
T(R2)0 = A1
I1 = 2
I0 = 1
Result = T(R1)3
1 T(R1)3 = Part[ T(R2)0, I0]
2 I3 = Length[ T(R2)0]
3 I4 = I0
4 goto 8
5 T(R1)2 = Part[ T(R2)0, I4]
6 T(R1)4 = T(R1)3 + T(R1)2
7 T(R1)3 = CopyTensor[ T(R1)4]]
8 if[ ++ I4 < I3] goto 5
9 Return
CompilePrint[fun2]
给出了
1 argument
5 Integer registers
4 Real registers
1 Tensor register
Underflow checking off
Overflow checking off
Integer overflow checking on
RuntimeAttributes -> {}
T(R1)0 = A1
I1 = 2
I0 = 1
Result = R2
1 R2 = Part[ T(R1)0, I0]
2 I3 = Length[ T(R1)0]
3 I4 = I0
4 goto 8
5 R1 = Part[ T(R1)0, I4]
6 R3 = R2 + R1
7 R2 = R3
8 if[ ++ I4 < I3] goto 5
9 Return
我选择包含这个而不是相当长的C版本,其中时序差异更加明显。
答案 0 :(得分:11)
您的附录实际上几乎足以看出问题所在。对于第一个版本,您在内部循环中调用CopyTensor
,而此是效率低下的主要原因,因为必须在堆上分配大量小缓冲区然后释放。为了说明,这是一个不复制的版本:
fun3 =
Compile[{{vec, _Real, 2}},
Module[{sum = vec[[1]], len = Length[vec[[1]]]},
Do[sum[[j]] += vec[[i, j]], {j, 1, len}, {i, 2, Length[vec]}];
sum], CompilationTarget -> "C"]
(顺便说一句,我认为编译为C时速度比较更公平,因为Mathematica虚拟机确实会阻止嵌套循环)。此功能仍然比你的慢,但比fun1
快3倍,对于这样的小矢量。
我认为其余的低效率是这种方法所固有的。事实上,你可以将问题分解为求解单个组件的总和,这是使你的第二个函数有效的原因,因为你使用像Transpose
这样的结构操作,最重要的是,这可以让你从中删除更多的指令。内循环。因为这是最重要的 - 你必须在内循环中尽可能少的指令。您可以从CompilePrint
看到fun1
vs fun3
确实存在这种情况。在某种程度上,您发现(对于此问题)一种有效的高级方式来手动展开外部循环(坐标索引上的循环)。您建议的另一种方法是让编译器根据向量维度的额外信息自动展开外部循环。这听起来似乎是合理的优化,但尚未实现Mathematica虚拟机的实现。
另请注意,对于较大长度的向量(比如20),fun1
和fun2
之间的差异消失了,因为张量复制中的内存分配/释放成本与成本相比变得无关紧要大规模赋值(当向量赋予向量时仍然可以更有效地实现 - 也许是因为在这种情况下你可以使用像memcpy
这样的东西)。
总而言之,我认为尽管自动进行优化会很自然,至少在这种特殊情况下,这是一种很难实现全自动的低级优化 - 甚至优化C编译器不要总是执行它。您可以尝试的一件事是将矢量长度硬编码到编译函数中,然后使用SymbolicCGenerate
(来自CCodeGenerator`
包)生成符号C,然后使用ToCCodeString
生成C代码(或者,您使用其他任何方式获取已编译函数的C代码),然后尝试手动创建和加载库,通过CreateLibrary
选项启用C编译器的所有优化。这是否有效我不知道。 EDIT 我实际上怀疑这会有所帮助,因为在生成C代码时,循环已经用goto
- s实现了速度,这可能会阻止编译器尝试循环展开。
答案 1 :(得分:5)
寻找一个能完全符合你想要的功能总是一个不错的选择。
In[50]:= fun3=Compile[{{vec,_Real,2}},Total[vec]]
Out[50]= CompiledFunction[{vec},Total[vec],-CompiledCode-]
In[51]:= Do[fun3[data],{10}]//Timing
Out[51]= {0.121982,Null}
In[52]:= fun3[data]===fun1[data]
Out[52]= True
另一种选择,效率较低(*由于转置*)是使用Listable
fun4 = Compile[{{vec, _Real, 1}}, Total[vec],
RuntimeAttributes -> {Listable}]
In[63]:= Do[fun4[Transpose[data]],{10}]//Timing
Out[63]= {0.235964,Null}
In[64]:= Do[Transpose[data],{10}]//Timing
Out[64]= {0.133979,Null}
In[65]:= fun4[Transpose[data]]===fun1[data]
Out[65]= True
答案 2 :(得分:1)
我怎么能告诉
Compile
它的一个参数不是一个任意的n * m矩阵,而是一个特殊的n * 2或n * 3,所以它可以自动进行这些优化而不是使用泛型向量加法函数添加微小的长度为2或长度为3的向量?
你正试图用勺子拯救泰坦尼克号!
在我的使用Mathematica 7.0.1的机器上,你的第一个例子需要4秒,你的第二个需要2秒而Total
需要0.1秒。所以你有一个客观上效率极低的解决方案(Total
快40倍!)并正确识别并解决了对性能不佳的最不重要贡献之一(使其快2倍)。
Mathematica的糟糕表现源于Mathematica代码的评估方式。从编程语言的角度来看,Mathematica的术语重写器可能是计算机代数的强大解决方案,但它对于通用程序评估而言效率极低。因此,像Mathematica那样的优化就像在2D编辑器中一样,不会像在编译语言中那样得到回报。
通过编写代码一次添加两个向量元素,我快速完成了Mathematica中的优化:
fun3 = Compile[{{vec, _Real, 2}},
Module[{x = vec[[1]][[1]], y = vec[[1]][[2]]},
Do[x += vec[[i]][[1]]; y += vec[[i]][[2]], {i, 2, Length[vec]}];
{x, y}]]
这实际上甚至更慢,耗时5.4秒。
但这是最糟糕的Mathematica。 Mathematica仅在以下情况下有用:
你真的不关心表现,或
Mathematica庞大的标准库已经包含了函数(在这种特定情况下就像Total
),可以让您通过一次调用或编写脚本来有效地解决问题。
正如ruebenko所说,Mathematica确实提供了一个内置函数来通过一次调用(Total
)为您解决这个问题,但在您询问的一般情况下这没有用。客观地说,最好的解决方案是通过将程序移植到更有效评估的语言来避免Mathematica的核心低效率。
例如,F#中最天真的可能解决方案(我必须手工编译的语言,但几乎任何其他人都会这样做)是使用2D数组:
let xs =
let rand = System.Random()
Array2D.init 1000000 2 (fun _ _ -> rand.NextDouble())
let sum (xs: float [,]) =
let total = Array.zeroCreate (xs.GetLength 1)
for i=0 to xs.GetLength 0 - 1 do
for j=0 to xs.GetLength 1 - 1 do
total.[j] <- total.[j] + xs.[i,j]
total
for i=1 to 10 do
sum xs |> ignore
这比您最快的解决方案快8倍!但是等等,你可以通过你自己的2D矢量类型利用静态类型系统做得更好:
[<Struct>]
type Vec =
val x : float
val y : float
new(x, y) = {x=x; y=y}
static member (+) (u: Vec, v: Vec) =
Vec(u.x+v.x, u.y+v.y)
let xs =
let rand = System.Random()
Array.init 1000000 (fun _ -> Vec(rand.NextDouble(), rand.NextDouble()))
let sum (xs: Vec []) =
let mutable u = Vec(0.0, 0.0)
for i=1 to xs.Length-1 do
u <- u + xs.[i]
u
此解决方案仅需0.057秒,比原始解决方案快70倍,并且比迄今为止发布的任何基于Mathematica的解决方案快得多!编译为高效SSE代码的语言可能会做得更好。
您可能认为70倍是一个怪胎特殊情况,但我已经看到很多将Mathematica代码移植到其他语言的例子给出了巨大的加速:
Sal Mangano的“表现至关重要”pricer for American options从移植到F#获得了960倍的加速。
Sal Mangano的red-black trees in Mathematica从移植到F#获得了100倍的加速。
我的ray tracer in Mathematica从移植到OCaml获得了100,000倍的加速。 Wolfram Research的Daniel Lichtblau后来对Mathematica进行了优化,但即使他的版本仍比我的OCaml慢1000倍。