繁体   English   中英

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

[英]Optimizing C code without parallel programming

我写的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代码的初学者

抱歉,这个问题太简单了

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

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

就是说,看起来您正在实施某种加密。 实际上,此代码看起来像是RC4等密码的精简版本。 如果您正在这样做,那么我会为您提供一些警告:

1)如果您要为生产代码编写加密,而这取决于您的安全性,则建议您使用知名且经过测试的库中的内容,而不是编写自己的库,这样会更快,更安全。

2)如果您要为生产代码编写自己的加密算法(而不只是“为了好玩”),请不要这样做。 安全的算法比任何人都可以设计的东西要多,您无法通过滚动自己获得任何东西。

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

大多数现代处理器只能根据源数据的就绪性来破坏指令的顺序,并乱序执行它们。 想想一个池,您将第一个〜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的跨步)上的简单流模式,无论是正方向还是负方向,但都可以轻松识别并且预取器应该开始触发,因此这些访问中的大多数都应该访问缓存。 另一方面,array1 / 2访问很复杂,因为它们是由加载结果决定的-这也会使程序停顿一些,但是在两种情况下都是一样的。

    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

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

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

循环1在索引到aa之前执行x[i] ^ x[i-1] ,而循环2在之前将x[i]索引aa ,然后在之后执行^ 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] ];
}

我不确定哪个会更快。

再次,有些人会说编译器的优化器会为您完成此操作,但是对其进行帮助没有害处。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM