强制GCC执行memcpy运行时大小检查的循环非开关?

是否有任何可靠的方法强制GCC(或任何编译器)在循环外的memcpy()memcpy()运行时大小检查(其中该大小不是编译时常量,但在该循环内是常量),专门针对每个循环相关的尺寸范围而不是反复检查其中的尺寸?

这是一个测试案例,从这里报告的性能回归中减少了一个开源库,该库旨在用于大数据集的高效内存分析。 (回归恰好是因为我的一个提交…)

原始代码在Cython中,但我已将其简化为纯C代理,如下所示:

 void take(double * out, double * in, int stride_out_0, int stride_out_1, int stride_in_0, int stride_in_1, int * indexer, int n, int k) { int i, idx, j, k_local; k_local = k; /* prevent aliasing */ for(i = 0; i < n; ++i) { idx = indexer[i]; for(j = 0; j < k_local; ++j) out[i * stride_out_0 + j * stride_out_1] = in[idx * stride_in_0 + j * stride_in_1]; } } 

步伐是可变的; 一般来说,数组甚至不能保证是连续的(因为它们可能是较大数组的非连续切片。)但是,对于c连续数组的特殊情况,我已将上述内容优化为以下内容:

 void take(double * out, double * in, int stride_out_0, int stride_out_1, int stride_in_0, int stride_in_1, int * indexer, int n, int k) { int i, idx, k_local; assert(stride_out_0 == k); assert(stride_out_0 == stride_in_0); assert(stride_out_1 == 1); assert(stride_out_1 == stride_in_1); k_local = k; /* prevent aliasing */ for(i = 0; i < n; ++i) { idx = indexer[i]; memcpy(&out[i * k_local], &in[idx * k_local], k_local * sizeof(double)); } } 

(断言不存在于原始代码中;而是检查连续性并在可能的情况下调用优化版本,如果不可能则调用未优化版本。)

在大多数情况下,此版本优化得非常好,因为正常用例如果是小n和大k 。 然而,相反的用例也确实发生了(大n和小k ),并且结果是n == 10000k == 4的特定情况(不能排除作为a的重要部分的代表)假设工作流程), memcpy()版本比原始版本慢3.6倍。 显然,这主要是因为k不是编译时常量,这可以通过下一个版本(几乎或完全取决于优化设置)以及原始(或更好,有时)执行这一事实来certificate,对于k == 4的特定情况:

  if (k_local == 4) { /* this optimizes */ for(i = 0; i < n; ++i) { idx = indexer[i]; memcpy(&out[i * k_local], &in[idx * k_local], k_local * sizeof(double)); } } else { for(i = 0; i < n; ++i) { idx = indexer[i]; memcpy(&out[i * k_local], &in[idx * k_local], k_local * sizeof(double)); } } 

显然,为k每个特定值硬编码循环是不切实际的,所以我尝试了以下(作为第一次尝试,以后可以通用化,如果它有效):

  if (k_local >= 0 && k_local <= 4) { /* this does not not optimize */ for(i = 0; i < n; ++i) { idx = indexer[i]; memcpy(&out[i * k_local], &in[idx * k_local], k_local * sizeof(double)); } } else { for(i = 0; i < n; ++i) { idx = indexer[i]; memcpy(&out[i * k_local], &in[idx * k_local], k_local * sizeof(double)); } } 

不幸的是,最后一个版本并不比原始的memcpy()版本快,这对我对GCC优化能力的信心有点令人沮丧。

有什么方法可以给GCC提供额外的“提示”(通过任何方式),这将有助于它在这里做正确的事情吗? (甚至更好,是否有“提示”可以在不同的编译器中可靠地工作?这个库是为许多不同的目标编译的。)

引用的结果是针对带有“-O2”标志的32位Ubuntu的GCC 4.6.3,但我也测试了具有相似(但不完全相同)结果的GCC 4.7.2和“-O3”版本。 我已将测试工具发布到LiveWorkspace ,但时间来自我自己的机器使用time(1)命令(我不知道LiveWorkspace时序有多可靠。)

编辑:我也考虑过设置一个最小尺寸的“神奇数字”来调用memcpy() ,我可以通过重复测试找到这样的值,但是我不确定我的结果是如何推广到不同的编译器/平台。 我可以在这里使用任何经验法则吗?

进一步编辑:在这种情况下实现了k_local变量是没用的,实际上,因为没有可能的混叠; 这可以从我在可能的地方进行的一些实验中减少( k是全局的)而我忘记了我改变了它。 只是忽略那部分。

编辑标签:已实现我也可以在较新版本的Cython中使用C ++,因此标记为C ++以防有任何可以帮助C ++的东西……

最终编辑:代替(现在)下载到专门的memcpy() ,以下似乎是我本地机器的最佳经验解决方案:

  int i, idx, j; double * subout, * subin; assert(stride_out_1 == 1); assert(stride_out_1 == stride_in_1); if (k < 32 /* ie 256 bytes: magic! */) { for(i = 0; i < n; ++i) { idx = indexer[i]; subout = &out[i * stride_out_0]; subin = &in[idx * stride_in_0]; for(j = 0; j < k; ++j) subout[j] = subin[j]; } } else { for(i = 0; i < n; ++i) { idx = indexer[i]; subout = &out[i * stride_out_0]; subin = &in[idx * stride_in_0]; memcpy(subout, subin, k * sizeof(double)); } } 

这使用了一个“幻数”来决定是否调用memcpy() ,但仍然优化了已知连续的小数组的情况(因此它比原始数据更快,在大多数情况下,因为原来没有这样做假设)。

最终,手头的问题是要求优化器基于多个变量做出关于运行时行为的假设。 虽然可以通过在关键变量上使用’const’和’register’声明来为优化器提供一些编译时提示,但最终,你依赖优化器做出很多假设。 此外,虽然memcpy()很可能是内在的,但它并不能保证,即使它/何时,实现可能会相当广泛地变化。

如果目标是实现最大性能,有时您只需要依靠技术来为您解决问题,而不是直接进行。 针对这种情况的最佳建议是使用内联汇编程序来解决问题。 这样做可以避免“黑匣子”解决方案的所有陷阱,使编译器和优化器的启发式方法得到补充,并有限地说明您的意图。 使用内联汇编程序的主要好处是能够避免在内存复制问题的解决方案中出现任何推送/弹出和无关的“泛化”代码,并能够直接利用处理器解决问题的能力。 不利的一面是维护,但考虑到你真的需要只针对大部分市场的英特尔和AMD,它并非不可克服。

我也可以补充一点,这个解决方案可以很好地利用多个内核/线程和/或GPU(如果/可用的话)并行进行复制并真正获得性能提升。 虽然延迟可能更高,但吞吐量也可能更高。 例如,如果你可以利用GPU,你可以在每个副本上启动一个内核,并在一次操作中复制数千个元素。

替代方法是依赖于编译器/优化器来为您做出最好的猜测,使用’const’和’register’声明,您可以在其中提供编译器提示并使用幻数基于“最佳解决方案”进行分支路径……然而,这将依赖于编译器/系统,并且从一个平台/环境到另一个平台/环境,您的里程会有很大差异。

SSE / AVX和对齐

例如,如果您使用的是现代英特尔处理器,则可以选择使用SSE或AVX指令。 虽然不是专门针对GCC,但请参阅此内容如果您感兴趣并使用缓存,我认为英特尔会为Linux和Windows编写一个版本的编译器套件,我想这附带了自己的库套件。

还有这篇文章 。

线程(eek)

我最近才遇到这种问题,memcpy()占用了太多时间。 在我的实例中,它是一个大的memcpy()(1MByte左右),而不是像你正在做的很多小的。

通过编写我自己的multithreadingmemcpy()来获得非常好的里程,其中线程是持久的,并且通过调用我自己的pmemcpy()函数来获得作业的“任务”。 持久线程意味着开销很低。 我获得了4核的x4改进。

因此,如果有可能将您的循环分解为合理数量的线程(我为每个可用核心分配了一个),并且您在计算机上拥有一些备用核心,您可能会获得类似的好处。

实时人群做什么 – DMA

顺便说一句,我很高兴能够玩一些非常奇特的OpenVPX硬件。 基本上它是一个大盒子里的一堆板子,它们之间有一个高速串行RapidIO互连。 每块板都有一个DMA引擎,可以将数据通过sRIO驱动到另一块板的内存。

我去的供应商非常聪明,如何最大限度地利用CPU。 聪明的一点就是DMA引擎非常聪明 – 它们可以被编程来执行诸如动态矩阵变换,条带挖掘,类似于你正在尝试的事情等等。并且因为它是CPU的独立硬件在此期间没有被束缚,所以可以忙着做别的事情。

例如,如果您正在进行像合成孔径雷达处理这样的操作,那么您总是会进行大矩阵变换。 美妙之处在于转换本身根本不占用CPU时间 – 您只需将数据移动到另一个板上即可实现转换。

无论如何,拥有这种东西的好处真的让人希望Intel CPU(和其他)具有能够工作内存而不仅仅是内存外设的板载DMA引擎。 这会让像你这样的任务变得非常快。

我认为最好的方法是尝试并找出最佳的“k”值,以便在原始算法(带有循环)和使用memcpy的优化算法之间切换。 最佳“k”在不同的CPU之间会有所不同,但不应该大不相同; 本质上是关于调用memcpy的开销,memcpy本身在选择最佳算法(基于大小,对齐等)与使用循环的“天真”算法时的开销。

memcpy是gcc中的内在函数,是的,但它不会做魔术。 它基本上做的是,如果size参数在编译时已知并且很小(我不知道阈值是多少),那么GCC将用内联代码替换对memcpy函数的调用。 如果在编译时不知道size参数,则将始终调用库函数memcpy。