首页 > 解决方案 > C++ Lambda 开销

问题描述

我有一个 O(N^4) 形式的缩放算法

...
...
...
for (unsigned i = 0; i < nI; ++i) {
  for (unsigned j = 0; j < nJ; ++j) {
    for (unsigned k = 0; k < nK; ++k) {
      for (unsigned l = 0; l < nL; ++l) {
        *calculate value*
        *do something with value*
      }   
    }
  }
}

我在几个地方需要这段代码,所以我把它looper作为一个类的一部分。这个loop函数是模板化的,所以它可以接受一个 lambda 函数来处理*do something with value*.

一些测试表明,这在性能方面并不是最佳的,但我不知道如何在每次需要时明确地写出这段代码。你看到这样做的方法吗?

标签: c++performancelambda

解决方案


使用模板化函数调用 lambda 应该生成可以被现代优化编译器优化的代码。GCC、Clang 和 MSVC 的最后一个版本实际上就是这种情况。您可以使用以下代码在GodBolt上进行检查:

extern int unknown1();
extern int unknown2(int);

template <typename LambdaType>
int compute(LambdaType lambda, int nI, int nJ, int nK, int nL)
{
    int sum = 0;

    for (unsigned i = 0; i < nI; ++i) {
        for (unsigned j = 0; j < nJ; ++j) {
            for (unsigned k = 0; k < nK; ++k) {
                for (unsigned l = 0; l < nL; ++l) {
                    sum += lambda(i, j, k, l);
                }   
            }
        }
    }

    return sum;
}


int caller(int nI, int nJ, int nK, int nL)
{
    int context = unknown1();

    auto lambda = [&](int i, int j, int k, int l) -> int {
        return unknown2(context + i + j + k + l);
    };

    return compute(lambda, nI, nJ, nK, nL);
}

使用优化标志,GCC、Clang 和 MSVC 能够生成compute在 4 个嵌套循环中省略 lambda 调用unknown2的有效实现(在生成的程序集中直接调用)。即使compute没有内联也是如此。请注意,lambda 捕获其上下文实际上并不会阻止优化这一事实(尽管编译器优化这种情况要困难得多)。

请注意,重要的是不要使用直接lambda 类型,而不是像包装器这样的包装器,std::function因为包装器可能会阻止优化(或至少使优化更难应用),从而导致直接函数调用。实际上,该类型帮助编译器内联函数,然后应用进一步的优化,如矢量化和常量传播

请注意,lambda 的代码应保持较小。否则,它可能不会被内联,从而导致函数调用。如果函数体在现代处理器上相当大,因为良好的分支预测单元和相对快速的大缓存,直接函数调用不会那么慢。然而,由于 lambda 内联,阻止进一步优化的成本可能是巨大的。降低此成本的一种方法是在 lambda 中移动至少一个循环(有关更多信息,请参阅面向数据的设计)。另一个解决方案是使用OpenMP来帮助编译器向量化 lambda,这要归功于#pragma omp declare simd [...]指令(假设你的编译器支持它)。您还可以使用编译器内联命令行参数来告诉您的编译器在这种情况下实际内联 lambda。


推荐阅读