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

时间:2013-03-21 17:46:20

标签: c++ c performance compiler-optimization memcpy

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

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

原始代码在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连续数组的特定情况,我已将上述内容优化为以下内容: / p>

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的特定情况(不可能是排除作为假设工作流程重要部分的代表,memcpy()版本比原始版本慢3.6倍。显然,这主要是由于k不是编译时常量这一事实,正如下一个版本执行(几乎或完全取决于优化设置)以及原始(或对于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的每个特定值对一个循环进行硬编码是不切实际的,所以我尝试了以下代码(作为第一次尝试,以后可以推广,如果有效的话):< / p>

    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优化能力的信心有点令人沮丧。

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

所引用的结果是针对32位Ubuntu的GCC 4.6.3,其中&#34; -O2&#34;旗帜,但我也测试了GCC 4.7.2和&#34; -O3&#34;具有相似(但不相同)结果的版本。我已将测试工具发布到LiveWorkspace,但时间来自我自己的机器,使用time(1)命令(我不知道LiveWorkspace时序的可靠性。)

编辑:我还考虑过设置一个&#34;幻数&#34;对于某个最小尺寸来调用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 /* i.e. 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));
        }
    }

这使用&#34;幻数&#34;决定是否调用memcpy(),但仍然优化已知连续的小数组的情况(因此它比原来快,在大多数情况下,因为原始没有这样的假设)。

3 个答案:

答案 0 :(得分:7)

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

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

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

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

答案 1 :(得分:2)

SSE / AVX和对齐

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

还有这个post

主题(eek)

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

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

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

实时人群做什么 - DMA

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

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

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

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

答案 2 :(得分:2)

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

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