1

我写了一个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- h 是一个字节值,在 loop1 和 loop2 中相同

我的问题是

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

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

非常感谢

我是 C 代码的初学者

对不起这个问题,如果它太容易了

4

4 回答 4

2

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

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

也就是说,它看起来有点像您正在实施某种加密。事实上,这段代码看起来像是 RC4 这样的密码的精简版。如果这就是你正在做的,我有几个警告给你:

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

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

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

于 2013-09-17T14:19:56.823 回答
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] . 这将 read-after-write 更改为 write-after-read,这要简单得多,因为加载比存储更早地在管道中完成。现在,允许处理器提前读取所有值(将它们保存在内部寄存器中的某个位置),并并行运行计算。写入被缓冲并在闲暇时完成,因为没有人依赖它们。

编辑:在某些情况下,另一个考虑因素是内存访问模式,但在这种情况下,它看起来像数组 x(1 宽步幅)上的简单流模式,无论是正方向还是负方向,但两者都可以很容易地识别和预取器应该开始提前触发,因此这些访问中的大多数应该会命中缓存。另一方面,array1/2 访问很复杂,因为它们是由加载的结果决定的——这也会让你的程序有点停顿,但在这两种情况下都是一样的。

于 2013-09-17T15:28:05.410 回答
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 循环的第一次迭代,值 array[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

于 2013-09-17T16:17:41.163 回答
0

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

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

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

无论如何,我会使用指针 for x[i]and 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] ];
}

我不确定哪个会更快。

再一次,有些人会说编译器的优化器会为你做这件事,但帮助它并没有什么坏处。

于 2013-09-17T19:48:47.683 回答