【问题标题】:Forcing GCC to perform loop unswitching of memcpy runtime size checks?强制 GCC 执行 memcpy 运行时大小检查的循环取消切换?
【发布时间】:2013-03-11 09:04:49
【问题描述】:

是否有任何可靠的方法可以强制 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 连续数组的特殊情况,我已将上述优化为以下内容:

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 的每个特定值硬编码一个循环是不切实际的,因此我尝试了以下方法(作为第一次尝试,如果可行,以后可以推广):

    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 /* 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));
        }
    }

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

【问题讨论】:

  • 我认为您描述的内存布局here 是一种病态的情况,它必然会产生大量的缓存和TLB 未命中。你能测量这些吗?
  • @MichaelFoukarakis 当然,对我应该尝试什么有什么建议吗?不太确定我应该在尝试中改变和尝试什么;对缓存问题没有太多经验。
  • 通常您希望为每个内存布局(行优先 (C)、列优先、Z 排序等其他)改变 (x, y) 维度并查看访问模式未命中。
  • 我使用PAPI 进行这种测量。至于优化memcpy,我认为你应该看看你的libc的源代码。
  • 你可以尝试反馈导向优化,也许 gcc 会用这些信息做一些聪明的事情?否则,我认为魔术截止是要走的路。

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


【解决方案1】:

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(和其他 CPU)具有能够工作内存-内存而不仅仅是内存外围设备的板载 DMA 引擎。这将使像您这样的任务变得非常快。

【讨论】:

  • 这是关于 memcpy 的一般信息,所以我为你 +1 努力了,但这真的适用于这种情况吗?大k 的情况很好(好吧,它们可以进行更多优化,但那是另一回事......);问题是如何避免小k 和大n 的性能回归而不强制截止值?
  • 我可能会求助于汇编,但是这个库通常作为独立于平台的 Python 和 Cython 源发布(Cython 被翻译成 C,然后在用户机器上使用它的主机编译器编译,除了在 Windows 上它通常是预先构建的)......所以了解汇编指令确实有帮助......但如果可能的话,我宁愿让GCC自己做合理的事情(以及让它在其他编译器上工作......这可能是不可能的,但谁知道呢?)
  • 另外,如果我为已知的处理器/编译器/平台组合执行此操作,则截止值是可以的,但我正在尝试使其尽可能便携
  • 好的,这通常会有所帮助,我可能会尝试将它用于大型数据集(其中任一维度都很大),但我认为它与手头的问题正交,因为我仍然希望每个人调用memcpy() 更快(或至少不比循环慢)或至少有一些可靠(跨平台)的方式来知道我应该调用memcpy() 还是显式循环。
  • 好吧,如果您将外部循环拆分为 4 个线程(假设它非常适合 n 的值),那将会有所帮助。是的,你仍然会经常调用 memcpy,但是你会有 4 个线程来做这件事,而不是一个。如果 n 太小但 k 大,那么你会在 4 个线程之间拆分内部循环。目标是让每个线程有足够的工作量。
【解决方案2】:

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

如果目标是实现最佳性能,有时您不必依赖技术来为您解决问题,而是直接执行。对于这种情况,最好的建议是使用内联汇编程序来解决问题。这样做可以让您避免“黑盒”解决方案的所有缺陷,这些缺陷与编译器和优化器的启发式方法相得益彰,并有限地陈述您的意图。使用内联汇编程序的主要好处是能够在解决内存复制问题时避免任何推送/弹出和无关的“泛化”代码,并且能够直接利用处理器解决问题的能力。不利的一面是维护,但考虑到您确实只需要解决英特尔和 AMD 即可覆盖大部分市场,这并非不可克服。

我还可以补充一点,如果/当可以并行复制并真正获得性能提升时,此解决方案可以让您充分利用多个内核/线程和/或 GPU。虽然延迟可能会更高,但吞吐量也很可能会更高。例如,如果您可以利用 GPU 的优势,那么您可以在每次复制时启动一个内核,并在一次操作中复制数千个元素。

对此的替代方法是依靠编译器/优化器为您做出最佳猜测,在您可以提供编译器提示的地方使用“const”和“register”声明,并使用幻数基于“最佳解决方案”路径...但是,这将非常依赖于编译器/系统,并且您的里程会因平台/环境而异。

【讨论】:

  • 答案不是为了讨论,而是为了回答提出的问题。在这种情况下,如果未来的谷歌人看到这篇文章,他需要经过多少“讨论”才能得到答案? Stack Overflow 的设置使得这个数字非常低。请编辑讨论部分,并将答案提炼成真正回答所提出问题的内容。
  • @KScottPiel,在 George 的编辑之后,这措辞有些误导​​(可以理解,因为 George 不是讨论的一部分)......如果你可以编辑它并澄清 const,@987654323 @,以及对优化器的其他提示在这种情况下实际上不起作用(不幸的是),我会接受它;否则(如果没有其他答案出现)我将不得不自己写一个更清晰的
  • 顺便说一句,使用 GPU 会使问题变慢。复制 RAM --PCIe--> GPU --PCIe--> RAM 比复制 RAM --> RAM 慢得多。 CPU 比 GPU 更快地访问 CPU 的 RAM。
  • 如果您正在处理大型数组,则不正确。如果您要遍历大量的小副本,那么每个副本都会产生大量开销。为了论证...假设您需要复制 2000 个元素,并且每次复制需要 {x} 时间才能在循环中完成...这使得整个数组的复制时间为 2000{x}。相反,您可以在 {a}ns 中将数组从 CPU 复制到 GPU,复制 {y} 中的每个元素并再次将数组复制回 {a}ns...使整个复制时间为 2{a} + {y }。哪个更快?
【解决方案3】:

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

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

【讨论】:

  • +1,但我并不是要“魔术”:这只是使用编译器完全知道的函数体进行循环取消切换!编译器拥有它需要知道的所有信息,k 介于 [0, 4] 之间,并且它可以在我的上一个版本中选择一个“小数组”版本......非常令人沮丧的是它不能使用这些信息...
  • @StephenLin:我认为问题在于这样的优化可能会使代码体积膨胀,以至于在一般情况下不值得这样做。
  • 我明白,但在这种情况下,我已经选择通过加倍来膨胀代码,编译器只是没有接受提示
  • 编译器使用了哪些优化相关的切换?例如‑O2 vs ‑O3‑ffast‑math(有点危险)。
猜你喜欢
  • 2010-12-21
  • 2018-04-10
  • 1970-01-01
  • 2014-10-19
  • 1970-01-01
  • 2016-05-25
  • 2011-04-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多