对于Julia使用基于单一的数组索引是否存在性能损失(无论多小),因为机器代码通常更直接支持从零开始的索引?
答案 0 :(得分:4)
我做了一些窥探,这是我喜欢的(我在下面的所有实验中使用了Julia 0.6):
> arr = zeros(5);
> @code_llvm arr[1]
define double @jlsys_getindex_51990(i8** dereferenceable(40), i64) #0 !dbg !5 {
top:
%2 = add i64 %1, -1
%3 = bitcast i8** %0 to double**
%4 = load double*, double** %3, align 8
%5 = getelementptr double, double* %4, i64 %2
%6 = load double, double* %5, align 8
ret double %6
}
在此代码段%1
中保存实际索引。请注意%2 = add i64 %1, -1
。 Julia确实在引擎盖下使用了基于0的数组,并从索引中减去了1。这导致生成额外的llvm指令,因此llvm代码看起来效率稍差。但是,这种额外的算术运算如何渗透到本机代码是另一个问题。
> @code_native arr[1]
.text
Filename: array.jl
Source line: 520
leaq -1(%rsi), %rax
cmpq 24(%rdi), %rax
jae L20
movq (%rdi), %rax
movsd -8(%rax,%rsi,8), %xmm0 # xmm0 = mem[0],zero
retq
L20:
pushq %rbp
movq %rsp, %rbp
movq %rsp, %rcx
leaq -16(%rcx), %rax
movq %rax, %rsp
movq %rsi, -16(%rcx)
movl $1, %edx
movq %rax, %rsi
callq 0xffffffffffcbf392
nopw %cs:(%rax,%rax)
这些架构的好消息是它们支持基于任意数字的索引。 movsd -8(%rax,%rsi,8), %xmm0
和leaq -1(%rsi), %rax
是受Julia中基于1的索引影响的两条指令。查看movsd
指令,在这一条指令中,我们同时执行实际索引和减法。 -8
部分是减法。如果使用基于0的索引而不是指令movsd (%rax,%rsi,8), %xmm0
。
另一个受影响的指令是leaq -1(%rsi), %rax
。但是由于cmp
指令使用in-out参数这一事实,%rsi
的值必须复制到另一个寄存器,因此在基于0的索引下仍会生成相同的指令,但它会可能看起来像leaq (%rsi), %rax
。
因此,在x86和amd64机器上,基于1的索引只会使用相同指令的稍微复杂的版本,但不会生成其他指令。代码最有可能与基于0的索引一样快。如果存在任何减速,则可能是由于特定的微架构并且将存在于一个CPU模型中而不存在于另一个CPU模型中。这种差异归结为芯片,我不担心。
不幸的是,我对arm
和其他架构知之甚少,但情况可能类似。
当与C或Python等其他语言交互时,总是必须记住在传递索引时减去或加1。编译器无法帮助您,因为其他代码超出了它的范围。因此,在这种情况下,存在1次提取算术运算的性能损失。但除非这是一个非常紧密的循环,否则这种差异可以忽略不计。
L20
标签下的所有内容。实际索引只是movq
和movsd
指令。因此,如果您关心真正快速的代码,那么绑定检查会比基于1的索引获得更多的性能损失。幸运的是,Julia提供了通过@inbound和--check-bounds=no
来缓解这些问题的方法。
答案 1 :(得分:3)
最有可能的是,Julia只是从你提供的索引中减去1,并在引擎盖下使用从零开始的数组。因此,性能损失将是减法的成本(几乎肯定是无关紧要的)。
编写两小段代码来测试每个代码的性能会很容易。