为啥将影响 lambda 的代码编译为 std::function 这么慢,尤其是使用 Clang?

Posted

技术标签:

【中文标题】为啥将影响 lambda 的代码编译为 std::function 这么慢,尤其是使用 Clang?【英文标题】:Why is compiling a code affecting lambda to std::function so slow, in particular with Clang?为什么将影响 lambda 的代码编译为 std::function 这么慢,尤其是使用 Clang? 【发布时间】:2019-03-01 03:12:10 【问题描述】:

我发现,将 lambda 函数转换为 std::function<> 值的相对少量代码的编译时间可能非常长,尤其是使用 Clang 编译器。

考虑以下创建 100 个 lambda 函数的虚拟代码:

#if MODE==1
#include <functional>
using LambdaType = std::function<int()>;
#elif MODE==2
using LambdaType = int(*)();
#elif MODE==3
#include "function.h" // https://github.com/skarupke/std_function
using LambdaType = func::function<int()>;
#endif

static int total=0;

void add(LambdaType lambda)

    total += lambda();


int main(int argc, const char* argv[])

    add([] return 1; );
    add([] return 2; );
    add([] return 3; );
    // 96 more such lines...
    add([] return 100; );

    return total == 5050 ? 0 : 1;

根据 MODE 预处理器宏,该代码可以在以下三种方式之间进行选择,以将 lambda 函数传递给 add 函数:

    std::function&lt;&gt;模板类 一个简单的 C 函数指针(这里可能只是因为没有捕获) 由 Malte Skarupke (https://probablydance.com/2013/01/13/a-faster-implementation-of-stdfunction/) 编写的 std::function 的快速替换

无论何种模式,程序总是以常规的0 错误代码退出。 但是现在看看 Clang 的编译时间:

$ time clang++ -c -std=c++11 -DMODE=1 lambdas.cpp 
real    0m8.162s
user    0m7.828s
sys 0m0.318s

$ time clang++ -c -std=c++11 -DMODE=2 lambdas.cpp 
real    0m0.109s
user    0m0.056s
sys 0m0.046s

$ time clang++ -c -std=c++11 -DMODE=3 lambdas.cpp 
real    0m0.870s
user    0m0.814s
sys 0m0.051s

$ clang++ --version
Apple LLVM version 10.0.0 (clang-1000.11.45.2)
Target: x86_64-apple-darwin17.7.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

哇。 std::function 和指向函数模式的指针之间有 80 倍的编译时间差异! std::function 和它的替代品之间甚至相差 10 倍。

怎么可能?是否存在特定于 Clang 的性能问题,还是由于 std::function 要求的固有复杂性?

我尝试使用 GCC 5.4 和 Visual Studio 2015 编译相同的代码。编译时间也有很大差异,但没有那么大。

GCC

$ time g++ -c -std=c++11 -DMODE=1 lambdas.cpp 
real    0m1.179s
user    0m1.080s
sys 0m0.092s

$ time g++ -c -std=c++11 -DMODE=2 lambdas.cpp 
real    0m0.136s
user    0m0.120s
sys 0m0.012s

$ time g++ -c -std=c++11 -DMODE=3 lambdas.cpp 
real    0m1.994s
user    0m1.792s
sys 0m0.196s

Visual Studio

C:\>ptime cl /c /DMODE=1 /EHsc /nologo lambdas.cpp
Execution time: 2.411 s

C:\>ptime cl /c /DMODE=2 /EHsc /nologo lambdas.cpp
Execution time: 0.270 s

C:\>ptime cl /c /DMODE=3 /EHsc /nologo lambdas.cpp
Execution time: 1.122 s

我现在正在考虑使用 Malte Skarupke 的实现,既要稍微提高运行时性能,又要大幅提高编译时间。

【问题讨论】:

你看过生成的程序集了吗?也许正在进行一些花哨的优化。 有趣的事实:使用-O3 gcc 将函数指针版本优化为 37 行汇编(在我的机器上)。我认为它可以在编译时计算结果 5050。作为比较:对于std::function,gcc 生成大约 8000 行汇编。使用std::function 时似乎涉及很多复杂性,这可以解释编译时间。 @pschill 有趣的观察,GCC 在某些情况下确实可以做出很好的优化!我故意测量编译时间而不进行优化。不,我没有考虑查看生成的代码进行比较。 【参考方案1】:

使用 --save-temps 选项查看编译器在每种情况下必须处理的内容。 在我的带有 clang 6.0.1 的机器上,MODE=1 会生成一个 575K 的预处理文件,因为其中包含大量标准库头文件。 MODE=1 生成一个 416 byte 文件,比它小 1000 倍。 生成的程序集也相差 10 倍。

【讨论】:

【参考方案2】:

我无法测试和解释您的示例,但是,从 Clang 9.0.0 开始,它可以对您的编译进行时间跟踪。请参阅phoronix article 了解印象和更多信息的链接。简而言之,您可以通过在命令行中添加 -ftime-trace 来获得它正在做什么的 json,您可以通过漂亮的图形将其可视化。

如果你发现一些非常奇怪的东西,你可以随时在 bugs.llvm.org 上记录一个错误,并且可以很好地重现(我认为改变这个问题的一些措辞会很好)

让我也添加一个关于测试代码的小评论。 std:: function 编译速度较慢我并不感到惊讶,因为这需要额外的包含来解析。 (并且包含的​​标准库非常庞大)此外,对于运行时,缓慢的影响是合乎逻辑的,因为 std:: function 添加了许多无法优化的额外间接。

我建议添加一个第 4 年的案例,其中 add 是一个模板,函数类型是模板参数:

template<typename LambdaType>
void add(LambdaType &&lambda)

    total += lambda();

【讨论】:

以上是关于为啥将影响 lambda 的代码编译为 std::function 这么慢,尤其是使用 Clang?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 std::vector 的速度是原始数组的两倍?包含完整代码

为啥将class反编译为java后,java直接编译时有错误

我可以将AS3代码或Flex项目交叉编译为本机C ++吗?

为啥我不能将 lambda 传递给这个需要 std::function 的函数? [复制]

为啥 gcc 将 _mm256_permute2f128_ps 编译为 Vinsertf128 指令?

为啥捕获 lambda 不能应用于 std::valarray?