无需并行编程即可优化C代码

时间:2013-09-17 14:00:33

标签: c optimization parallel-processing

我写了一个

的C代码
for(i=1;i<10000;i++)
    x[i]=array1[h][x[i]^x[i-1]]

for(i=9999;i>0;i--)
    x[i]=x[i-1]^array2[h][x[i]]

注意:

1- array1和array2包含字节值

2-秒循环执行第一个循环的相反功能

3小时是一个字节值,在loop1和loop2中是相同的

我的问题是

第二个循环比第一个循环快,我理解这一点,因为在第一个循环中,x中的每个值都取决于前一个字节IE的新值。要计算x2,你必须计算x1,而在第二个循环中,每个字节取决于已经存在的前一个字节的旧值IE。要计算x9999,你需要x9998的旧值而不是新值,因此无需等待x9999的计算,如何在C代码中完成,以及所谓的并行编程,这意味着C语言为某些循环进行并行编程如果没有用户控制和编写这样的并行

,那就不顺序了

问题是: 为什么2.循环比1.循环更快?

非常感谢

我是C代码的初学者

对于这个问题很抱歉

4 个答案:

答案 0 :(得分:2)

您的第一个循环取决于先前迭代的结果。这意味着,简而言之,处理器在完成i=2之前无法开始考虑i=1,因为x[2]取决于x[1]。但是,第二个循环不依赖于先前迭代的结果。

通过添加-O3标志(即大写'o'而非零)来启用编译器优化可以加快两个循环并使它们更接近相同的速度。有“手动”优化,如循环矢量化或使用更广泛的数据类型,您仍然可以实现,但首先尝试-O3标志。如果您不知道如何执行此操作,请查看IDE的“编译器标志”帮助文件。

那就是说,看起来有点像你正在实施某种加密。实际上,这段代码看起来像RC4这样的密码的精简版本。如果这就是你正在做的事情,我会给你一些警告:

1)如果您正在为生产代码编写加密,那么您依赖于安全性,我建议您使用来自知名且经过测试的库中的某些内容而不是编写自己的内容,它会更快,更安全

2)如果您正在为生产代码编写自己的加密算法(而不仅仅是“为了好玩”),请不要。有比任何一个人都可以设计的任何东西更安全的算法,你不会通过自己的方式获得任何东西。

3)如果你正在编写或实现一个有趣的算法,那么对你好!一旦你完成了一些现实世界的实现,你可能会发现一些好主意。

答案 1 :(得分:1)

大多数现代处理器只能根据源数据的准备情况来破坏指令的顺序,并且无序地执行它们。想想你将第一次~50次迭代注入稳定状态的池(可能比执行速度快) - 假设你有多个ALU,你可以开始并行执行多少次?在某些情况下,您甚至可以并行化所有代码,使您受执行资源数量的限制(可能非常高)。编辑:重要的是要注意这在复杂的控制流程中变得更加困难(例如,如果你的循环中有一堆if条件,特别是如果它们依赖于数据),因为你需要预测它们并冲洗年轻的指令错了..

一个好的编译器也可以在该循环展开和向量化之上添加,这进一步增强了这种并行性和可以从CPU实现的执行BW。

Dan对依赖完全正确(虽然它不是一个简单的“管道”)。在第一个循环中,每次迭代的x [i-1]将被识别为前一个x [i]的别名(通过CPU别名检测),使其成为写后读取方案并强制执行等待和转发结果(跨越多次迭代,这形成一个长链依赖 - 虽然你可以看到迭代N,你不能执行它,直到你完成N-1,等待N-2,所以上..)。顺便说一句,如果复杂转发的情况,例如缓存行拆分或页面拆分访问,这可能会变得更加糟糕。

第二个循环也使用其他单元格中的值,但是有一个重要的区别 - 程序顺序首先读取x [i-1]的值(用于计算x [i]),然后才写入x [i -1]。这样可以将写后读写更改为写后读操作,这更加简单,因为加载比管道更早地沿着管道完成。现在,允许处理器提前读取所有值(将它们保存在内部寄存器中),并且并行运行计算。写作是缓冲的,并且在闲暇时完成,因为没有人依赖它们。

编辑: 在某些情况下,另一个考虑因素是内存访问模式,但在这种情况下,它看起来像一个简单的流模式而不是数组x(1-wide stride),无论是正方向还是负方向,但两者都可以轻松识别,预取器应该启动向前发射,因此大多数这些访问应该到达缓存。 另一方面,array1 / 2访问很复杂,因为它们是由负载的结果决定的 - 这也会使你的程序停顿一下,但在两种情况下都是一样的。

答案 2 :(得分:0)

    for(i=1;i<10000;i++)
        x[i]=array1[h][x[i]^x[i-1]]

for循环的每次迭代都需要从array1获取一个值。无论何时访问值,都会读取此值周围的数据(通常是高速缓存行大小)并将其存储在高速缓存中。 L1和L2缓存的缓存行大小不同,我认为它们分别是64字节和128字节。下次当您访问前一个值周围的相同数据或数据时,您很可能会发生缓存命中,从而使您的操作速度提高一个数量级。

现在,在上面的for循环中,x [i] ^ x [i-1]可以评估为其值不在连续迭代的高速缓存行的大小内的数组索引。让我们以L1缓存为例。对于for循环的第一次迭代,访问值数组[h] [x [i] ^ x [i-1]],它位于主存储器中。围绕该字节值的64字节数据被带入并存储在L1高速缓存中的高速缓存行中。对于下一次迭代,x [i] ^ x [i-1]可以导致索引,其值存储在不在第一次迭代中引入的64字节附近的位置。因此,再次访问高速缓存未命中和主存储器。在执行for循环期间,这可能会多次发生,从而导致性能不佳。

尝试查看x [i] ^ x [i-1]为每次迭代求值的内容。如果它们大不相同,那么部分缓慢是由于上述原因造成的。

以下链接很好地解释了这个概念。

http://channel9.msdn.com/Events/Build/2013/4-329

答案 3 :(得分:0)

在这两种情况下,您都应该说unsigned char * aa = &array1[h];(或array2[h]作为第二个循环)。希望编译器提升索引操作是没有意义的,当你可以做到并且确定时。

这两个循环正在做不同的事情:

循环1在索引到x[i] ^ x[i-1]之前执行aa,而循环2在aa之前将x[i]编入索引,然后执行^ x[i-1]

无论如何,我会使用x[i]x[i-1]的指针,我会展开循环,所以循环1看起来像这样:

unsigned char * aa = &array1[h];
unsigned char * px = &x[1];
unsigned char * px1 = &x[0];
for (i = 1; i < 10; i++){
   *px = aa[ *px ^ *px1 ]; px++; px1++;
}
for ( ; i < 10000; i += 10 ){
   *px = aa[ *px ^ *px1 ]; px++; px1++;
   *px = aa[ *px ^ *px1 ]; px++; px1++;
   *px = aa[ *px ^ *px1 ]; px++; px1++;
   *px = aa[ *px ^ *px1 ]; px++; px1++;
   *px = aa[ *px ^ *px1 ]; px++; px1++;
   *px = aa[ *px ^ *px1 ]; px++; px1++;
   *px = aa[ *px ^ *px1 ]; px++; px1++;
   *px = aa[ *px ^ *px1 ]; px++; px1++;
   *px = aa[ *px ^ *px1 ]; px++; px1++;
   *px = aa[ *px ^ *px1 ]; px++; px1++;
}

另一种方法是使用单个p指针,并使用硬偏移,如下所示:

unsigned char * aa = &array1[h];
unsigned char * px = &x[0];
for (i = 1; i < 10; i++){
   px[1] = aa[ px[1] ^ px[0] ]; px++;
}
for ( ; i < 10000; i += 10, px += 10 ){
   px[ 1] = aa[ px[ 1] ^ px[0] ];
   px[ 2] = aa[ px[ 2] ^ px[1] ];
   px[ 3] = aa[ px[ 3] ^ px[2] ];
   px[ 4] = aa[ px[ 4] ^ px[3] ];
   px[ 5] = aa[ px[ 5] ^ px[4] ];
   px[ 6] = aa[ px[ 6] ^ px[5] ];
   px[ 7] = aa[ px[ 7] ^ px[6] ];
   px[ 8] = aa[ px[ 8] ^ px[7] ];
   px[ 9] = aa[ px[ 9] ^ px[8] ];
   px[10] = aa[ px[10] ^ px[9] ];
}

我不确定哪个更快。

同样,有些人会说编译器的优化器会为你做这件事,但是帮助它并没有什么害处。