在某些情况下,用FORTRAN系统编译出来的程序会比用C/C++系统编译出来的程序运行得更快。例如,下面这个例子就是如此:
void f1 (int* x, int* y, int* sum, int* product, int* sum_product, int* update_me, size_t s)
{
for (int i=0; i<s; i++)
{
sum[i]=x[i]+y[i];
product[i]=x[i]*y[i];
update_me[i]=i*123; // some dummy value
sum_product[i]=sum[i]+product[i];
};
};
这个程序的功能十分简单,但是里面的指针问题却发人深思:同一块内存可以由多个指针来访问,因此同一个地址的数据可能会被多个指针轮番复写。至少现行标准并不禁止这种情况。
C语言编译器完全允许上述情况。因此,它分四个阶段处理每次迭代的各类数组:
制备sum[i];
制备product[i];
制备update_me[i];
制备sum_product[i]。在这个阶段,计算机将从内存里重新加载sum[i]和product[i]。
第四个阶段是否存在进一步优化的空间呢?既然前面已经计算好了sum[i]和product[i],那么后面我们应该就不必再从内存中读取它们的值了。
答案是肯定的。
只是编译器本身并不能在第三个阶段确定前两个阶段的赋值没有被其他指令覆盖。换而言之,因为编译器不能判断该程序里是否存在指向相同内存区域的指针——即“指针别名(pointer aliasing)”,所以编译器不能确保该指针指向的内存没被改写。
C99标准中的受限指针[ISO07,6.7.3节](部分文献又称“严格别名”)的应运而生。编程人员可通过受限指针的strict修饰符向编译器承诺:被该关键字标记的指针是操作相关内存区域的唯一指针,没有其他指针重复指向这个指针所操作的内存区域。
用更为确切、更为正式的语言来说,关键字“restrict”表示该指针是访问既定对象的唯一指针,其他指针都不会重复操作既定对象。从另一个角度来看,一旦某个指针被标记为受限指针,那么编译器就认定既定对象只会被指定的受限指针操作。
下面我们将为每个指针都增加上restrict修饰符:
void f2 (int* restrict x, int* restrict y, int* restrict sum, int* restrict product, int* ↙
↘ restrict sum_product,
int* restrict update_me, size_t s)
{
for (int i=0; i<s; i++)
{
sum[i]=x[i]+y[i];
product[i]=x[i]*y[i];
update_me[i]=i*123; // some dummy value
sum_product[i]=sum[i]+product[i];
};
};
我们看到的结果如下所示。
指令清单44.1 x64下的GCC函数f1()
f1:
push r15 r14 r13 r12 rbp rdi rsi rbx
mov r13, QWORD PTR 120[rsp]
mov rbp, QWORD PTR 104[rsp]
mov r12, QWORD PTR 112[rsp]
test r13, r13
je .L1
add r13, 1
xor ebx, ebx
mov edi, 1
xor r11d, r11d
jmp .L4
.L6:
mov r11, rdi
mov rdi, rax
.L4:
lea rax, 0[0+r11*4]
lea r10, [rcx+rax]
lea r14, [rdx+rax]
lea rsi, [r8+rax]
add rax, r9
mov r15d, DWORD PTR [r10]
add r15d, DWORD PTR [r14]
mov DWORD PTR [rsi], r15d ; store to sum[]
mov r10d, DWORD PTR [r10]
imul r10d, DWORD PTR [r14]
mov DWORD PTR [rax], r10d ; store to product[]
mov DWORD PTR [r12+r11*4], ebx ; store to update_me[]
add ebx, 123
mov r10d, DWORD PTR [rsi] ; reload sum[i]
add r10d, DWORD PTR [rax] ; reload product[i]
lea rax, 1[rdi]
cmp rax, r13
mov DWORD PTR 0[rbp+r11*4], r10d ; store to sum_product[]
jne .L6
.L1:
pop rbx rsi rdi rbp r12 r13 r14 r15
ret
指令清单44.2 x64下的GCC函数f2()
f2:
push r13 r12 rbp rdi rsi rbx
mov r13, QWORD PTR 104[rsp]
mov rbp, QWORD PTR 88[rsp]
mov r12, QWORD PTR 96[rsp]
test r13, r13
je .L7
add r13, 1
xor r10d, r10d
mov edi, 1
xor eax, eax
jmp .L10
.L11:
mov rax, rdi
mov rdi, r11
.L10:
mov esi, DWORD PTR [rcx+rax*4]
mov r11d, DWORD PTR [rdx+rax*4]
mov DWORD PTR [r12+rax*4], r10d ; store to update_me[]
add r10d, 123
lea ebx, [rsi+r11]
imul r11d, esi
mov DWORD PTR [r8+rax*4], ebx ; store to sum[]
mov DWORD PTR [r9+rax*4], r11d ; store to product[]
add r11d, ebx
mov DWORD PTR 0[rbp+rax*4], r11d ; store to sum_product[]
lea r11, 1[rdi]
cmp r11, r13
jne .L11
.L7:
pop rbx rsi rdi rbp r12 r13
ret
f1()函数和f2()函数的不同之处在于:在f1()函数中,sum[i]和product[i]数组在循环中会再次加载;而函数f2()则没有这种重新加载内存数值的操作。在改动后的程序里,因为我们向编译器“承诺”sum[i]和product[i]的值不会被其他指针复写,所以计算机会重复利用前几个阶段制备好的各项数据,不再从内存加载它们的值了。很明显,改进后的程序运行速度更快一些。
如果我们声明了某个指针是受限指针,而实际的程序又有其他指针操作这个受限指针操作的内存区域,将会发生什么情况?这真的就是程序员的事了,不过程序运行的结果肯定是错误的。
FORTRAN语言的编译器把所有指针都视为受限指针。因此,在C语言不支持C99标准的restrict修饰符而实际指针属于受限指针的时候,用FORTRAN语言编译出来的应用程序会比用C语言编译出来的程序运行得更快。
受限指针主要用于哪些领域?它主要用于操作多个大尺寸内存块的应用方面。例如,在超级计算机/HPC平台上经常进行的线性方程组求解就属于这种类型的应用。或许,这正是这种平台普遍采用FORTRAN语言的原因之一吧。
另一方面,在循环语句的迭代次数不是非常高的情况下,受限指针带来的性能提升就不会十分明显。