我做了一些窥探,这是我喜欢的(我在下面的所有实验中都使用了 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
}
Run Code Online (Sandbox Code Playgroud)
在这个片段中%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)
Run Code Online (Sandbox Code Playgroud)
这些架构的好消息是它们支持基于任意数字的索引。该movsd -8(%rax,%rsi,8), %xmm0和leaq -1(%rsi), %rax是受在朱莉娅的基于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 次提取算术运算的性能会受到影响。但除非这是一个非常紧密的循环,否则这种差异可以忽略不计。
嗯,房间里的大象是边界检查。回到前面的汇编代码段,大部分生成的代码都与此相关 - 前 3 条指令以及L20标签下的所有内容。实际的索引只是movq和movsd说明。因此,如果您关心真正快速的代码,那么与基于 1 的索引相比,边界检查会带来更多的性能损失。幸运的是,Julia 提供了通过使用@inbound和--check-bounds=no.
最有可能的可能性是 Julia 只是从您提供的索引中减去 1,并在幕后使用从零开始的数组。因此,性能损失将是减法的成本(几乎肯定是无关紧要的)。
编写两小段代码来测试每段代码的性能是很容易的。