- 第一章 CPU 简介
- 第二章 Hello,world!
- 第三章 函数开始和结束
- 第四章 栈
- Chapter 5 printf() 与参数处理
- Chapter 6 scanf()
- CHAPER7 访问传递参数
- Chapter 8 一个或者多个字的返回值
- Chapter 9 指针
- Chapter 10 条件跳转
- 第 11 章 选择结构 switch()/case/default
- 第 12 章 循环结构
- 第 13 章 strlen()
- Chapter 14 Division by 9
- chapter 15 用 FPU 工作
- Chapter 16 数组
- Chapter 17 位域
- 第 18 章 结构体
- 19 章 联合体
- 第二十章 函数指针
- 第 21 章 在 32 位环境中的 64 位值
- 第二十二章 SIMD
- 23 章 64 位化
- 24 章 使用 x64 下的 SIMD 来处理浮点数
- 25 章 温度转换
- 26 章 C99 的限制
- 27 章 内联函数
- 第 28 章 得到不正确反汇编结果
- 第 29 章 花指令
- 第 30 章 16 位 Windows
- 第 31 章 类
- 三十二 ostream
- 34.2.2 MSVC
- 34.2.3 C++ 11 std::forward_list
- 34.3 std::vector
- 34.4 std::map and std::set
26 章 C99 的限制
这个例子说明了为什么某些情况下 FORTRAN 的速度比 C/C++要快
#!cpp
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];
};
};
这是一个十分简单的例子,但是有一点需要注意:指向 update_me 数组的指针也可以指向 sum 数组,甚至是 sum_product 数组。但是这不是严重的错误,对吗? 编译器很清楚这一点,所以他在循环体中产生了四个阶段: 1.计算下一个 sum[i] 2.计算下一个 product[i] 3.计算下一个 unpdate_me[i] 4.计算下一个 sum_product[i],在这个阶段,我们需要从已经计算过 sum[i]和 product[i]的内存中载入数据
最后一个阶段可以优化吗?既然已经计算过的 sum[i]和 product[i]是不需要再次从内存装载的(因为我们已经计算过他们了)。但是编译器不能保证在第三个阶段没有东西被覆盖掉!这就叫“指针别名”,在这种情况下编译器无法确定指针指向区域的内存是否已经被改变。
C99 标准中的限制给解决这一问题带来了一线曙光。由设计器传送给编译器的函数单元在标记这种关键字(restrict) 后,它会指向不同的内存区域,并且不 会被混用。 如果要更加准确地描述这种情况,restrict 表明了只有指针是可以访问对象的。这样的话我们可以通过特定的指针进行工作,并且不会用到其他指针。也就是说一个对象如果被标记为 restrict,那么它只能通过一个指针访问。 我们把每个指向变量的指针标记为 restrict 关键字:
#!cpp
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];
};
};
来看下结果:
清单 26.1: GCC x64: f1()
#!bash
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
清单 26.2: GCC x64: f2()
#!bash
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。FORTRAN 语言编译器按照指针的本身含义对待他,所以当 FORTRAN 程序在这种情况下不可能使用 restrict 的时候,它可以生成生成执行更快的代码。
这有什么实用价值?当函数处理内存中很多大“块”的时候,比如说用超级计算机解决线性代数问题。或许这就是为什么 FORTRAN 语言还在这个领域被使用。 但是当迭代步骤不是很多的时候,速度的增加并不是显著的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论