展开循环(C)
我理解展开循环的概念,但是,有人能给我解释一下如何展开一个简单的循环吗 如果你能给我看一个循环,然后是一个循环的展开版本,并解释正在发生的事情,那就太好了展开循环(C),c,loops,performance-testing,unroll,C,Loops,Performance Testing,Unroll,我理解展开循环的概念,但是,有人能给我解释一下如何展开一个简单的循环吗 如果你能给我看一个循环,然后是一个循环的展开版本,并解释正在发生的事情,那就太好了 #define N 44 int main() { int A[N], B[N]; int i; // fill A with stuff ... for(i = 0; i < N; i++) { B[i] = A[i] * (100 % i); } // do
#define N 44
int main() {
int A[N], B[N];
int i;
// fill A with stuff ...
for(i = 0; i < N; i++) {
B[i] = A[i] * (100 % i);
}
// do stuff with B ...
}
展开:
#define N 44
int main() {
int A[N], B[N];
int i;
// fill A with stuff ...
for(i = 0; i < N; i += 4) {
B[i] = A[i] * (100 % i);
B[i+1] = A[i+1] * (100 % i+1);
B[i+2] = A[i+2] * (100 % i+2);
B[i+3] = A[i+3] * (100 % i+3);
}
// do stuff with B ...
}
展开可能会以更大的程序大小为代价来提高性能。性能的提高可能是由于分支惩罚、缓存未命中和执行指令的减少。有些缺点是显而易见的,比如代码量的增加和可读性的降低,有些则不那么明显。
#define N 44
int main() {
int A[N], B[N];
int i;
// fill A with stuff ...
for(i = 0; i < N; i++) {
B[i] = A[i] * (100 % i);
}
// do stuff with B ...
}
展开:
#define N 44
int main() {
int A[N], B[N];
int i;
// fill A with stuff ...
for(i = 0; i < N; i += 4) {
B[i] = A[i] * (100 % i);
B[i+1] = A[i+1] * (100 % i+1);
B[i+2] = A[i+2] * (100 % i+2);
B[i+3] = A[i+3] * (100 % i+3);
}
// do stuff with B ...
}
展开可能会以更大的程序大小为代价来提高性能。性能的提高可能是由于分支惩罚、缓存未命中和执行指令的减少。有些缺点是显而易见的,比如代码量增加和可读性降低,有些则不那么明显。展开循环的过程利用了计算机科学中的一个基本概念:时空权衡,增加使用的空间通常会导致算法的时间缩短 假设我们有一个简单的循环
const int n = 1000;
for (int i = 0; i < n; ++i) {
foo();
}
因此,时空折衷是对执行的~4*1000=~4000条指令进行5行汇编
那么,让我们试着稍微展开这个循环
for (int i = 0; i < n; i += 10) {
foo();
foo();
foo();
foo();
foo();
foo();
foo();
foo();
foo();
foo();
}
对于执行的~14*100=~1400条指令,时空折衷是14行汇编
我们可以进行完全展开,如下所示:
mov eax, 0
loop:
call foo
inc eax
cmp eax, 1000
jne loop
foo();
foo();
// ...
// 996 foo()'s
// ...
foo();
foo();
它在汇编中编译为1000条调用指令
这为1000条指令提供了1000行汇编的时空权衡
如您所见,总的趋势是为了减少CPU执行的指令量,我们必须增加所需的空间
完全展开循环是没有效率的,因为所需的空间变得非常大。部分展开带来了巨大的好处,展开循环越多,回报就会大大减少
虽然理解循环展开是一个好主意,但请记住,编译器是智能的,会为您完成这项工作。展开循环的过程利用了计算机科学中的一个基本概念:时空权衡,增加使用的空间通常会导致减少算法的时间 假设我们有一个简单的循环
const int n = 1000;
for (int i = 0; i < n; ++i) {
foo();
}
因此,时空折衷是对执行的~4*1000=~4000条指令进行5行汇编
那么,让我们试着稍微展开这个循环
for (int i = 0; i < n; i += 10) {
foo();
foo();
foo();
foo();
foo();
foo();
foo();
foo();
foo();
foo();
}
对于执行的~14*100=~1400条指令,时空折衷是14行汇编
我们可以进行完全展开,如下所示:
mov eax, 0
loop:
call foo
inc eax
cmp eax, 1000
jne loop
foo();
foo();
// ...
// 996 foo()'s
// ...
foo();
foo();
它在汇编中编译为1000条调用指令
这为1000条指令提供了1000行汇编的时空权衡
如您所见,总的趋势是为了减少CPU执行的指令量,我们必须增加所需的空间
完全展开循环是没有效率的,因为所需的空间变得非常大。部分展开带来了巨大的好处,展开循环越多,回报就会大大减少
虽然理解循环展开是一个好主意,但请记住,编译器是智能的,并且会为您完成这项工作。我认为澄清什么时候循环展开最有效很重要:使用依赖链。依赖链是一系列操作,其中每个计算都依赖于以前的计算。例如,下面的循环有一个依赖链
for(i=0; i<n; i++) sum += a[i];
没有依赖链,因此无序执行没有问题。但是,让我们考虑一个指令集,它需要每次迭代的以下操作。
2 SIMD load
1 SIMD store
1 SIMD multiply
1 SIMD addition
1 scalar addition for the loop counter
1 conditional jump
我们还假设处理器每个周期可以执行其中五条指令。在这种情况下,每个迭代有七条指令,但每个周期只能执行五条指令。然后可以使用循环展开将标量加法的成本分摊到计数器i和条件跳转中。例如,如果完全展开循环,则不需要这些指令
对于摊销循环计数器和跳跃-funroll循环的成本,GCC可以很好地工作。它展开八次,这意味着计数器加法和跳转必须每八次迭代进行一次,而不是每一次迭代。我认为澄清什么时候循环展开最有效很重要:使用依赖链。依赖链是一系列操作,其中每个计算都依赖于以前的计算。例如,下面的循环有一个依赖链
for(i=0; i<n; i++) sum += a[i];
2 SIMD load
1 SIMD store
1 SIMD multiply
1 SIMD addition
1 scalar addition for the loop counter
1 conditional jump
没有依赖链,因此无序执行没有问题。但是,让我们考虑一个指令集,它需要每次迭代的以下操作。
2 SIMD load
1 SIMD store
1 SIMD multiply
1 SIMD addition
1 scalar addition for the loop counter
1 conditional jump
我们还假设处理器每个周期可以执行其中五条指令。在这种情况下,每个iter有七条指令
但每个循环只能进行五次。然后可以使用循环展开将标量加法的成本分摊到计数器i和条件跳转中。例如,如果完全展开循环,则不需要这些指令
对于摊销循环计数器和跳跃-funroll循环的成本,GCC可以很好地工作。它展开八次,这意味着计数器的加法和跳转必须每八次迭代进行一次,而不是每一次迭代。你读过维基百科页面吗?这里有一些很好的c示例。其基本思想是,展开的循环在读取/写入您关心的数据方面花费的周期比检查/更新控制循环的条件变量相关的开销要多,因此可以提供更好的性能:另请参见:您阅读过wikipedia页面吗?这里有一些很好的c示例。基本思想是,展开的循环在读/写您关心的数据时所花费的周期比检查/更新控制循环的条件变量时所花费的开销要多,因此可以提供更好的性能:另请参见:您不应假设编译器将展开循环。在依赖链中,GCC不会展开循环。即使使用-funroll循环,也不会破坏依赖链。手动循环展开在优化中仍然很有用。回答不错。我想知道循环展开是否是大多数编译语言或C&related语言的常见特性。而且,我猜用解释语言展开是不可能的,对吗?很抱歉,如果我在非主题中有点分歧,您不应该假设编译器将展开循环。在依赖链中,GCC不会展开循环。即使使用-funroll循环,也不会破坏依赖链。手动循环展开在优化中仍然很有用。回答不错。我想知道循环展开是否是大多数编译语言或C&related语言的常见特性。而且,我猜用解释语言展开是不可能的,对吗?对不起,如果我在离题的问题上有点分歧
2 SIMD load
1 SIMD store
1 SIMD multiply
1 SIMD addition
1 scalar addition for the loop counter
1 conditional jump