【问题标题】:Are measurable performance gains possible from using VC++'s __assume?使用 VC++ 的 __assume 是否可以获得可衡量的性能提升?
【发布时间】:2012-03-19 05:44:18
【问题描述】:

使用 VC++ 的__assume 是否可以获得可衡量的性能提升?如果是这样,请在您的答案中发布带有代码和基准的证明。

关于 __assume 的稀疏 MSDN 文章:http://msdn.microsoft.com/en-us/library/1b3fsfxw(v=vs.100).aspx

文章中提到使用__assume(0) 使switch 语句通过__assume(0) 处理default 的情况更快。我测量到以这种方式使用 __assume(0) 并没有提高性能:

void NoAssumeSwitchStatement(int i)
{
    switch (i)
    {
    case 0:
        vector<int>();
        break;
    case 1:
        vector<int>();
        break;
    default:
        break;
    }
}

void AssumeSwitchStatement(int i)
{
    switch (i)
    {
    case 0:
        vector<int>();
        break;
    case 1:
        vector<int>();
        break;
    default:
        __assume(0);
    }
}

int main(int argc, char* argv[])
{
    const int Iterations = 1000000;
    LARGE_INTEGER start, middle, end;
    QueryPerformanceCounter(&start);
    for (int i = 0; i < Iterations; ++i)
    {
        NoAssumeSwitchStatement(i % 2);         
    }
    QueryPerformanceCounter(&middle);
    for (int i = 0; i < Iterations; ++i)
    {
        AssumeSwitchStatement(i % 2);
    }
    QueryPerformanceCounter(&end);
    LARGE_INTEGER cpuFrequency;
    QueryPerformanceFrequency(&cpuFrequency);
    cout << "NoAssumeSwitchStatement: " << (((double)(middle.QuadPart - start.QuadPart)) * 1000) / (double)cpuFrequency.QuadPart << "ms" << endl;
    cout << "  AssumeSwitchStatement: " << (((double)(end.QuadPart - middle.QuadPart)) * 1000) / (double)cpuFrequency.QuadPart << "ms" << endl;
    return 0;
}

圆形控制台输出,1000000 次迭代:

NoAssumeSwitchStatement:46 毫秒
AssumeSwitchStatement:46ms

【问题讨论】:

  • 你的例子似乎太琐碎了。如果您查看汇编程序的输出,它会是一样的。没有。
  • IMO,当编译器知道无法达到默认情况时,您将其排除在外而不是枯燥的假设指令时,这会更有意义......
  • __assume 是对优化器的提示,在更高级别的优化中最有用。 (即超出寄存器分配和 CSE)。在这些级别上,可以预期这个示例是完全内联的;即您的 2 个函数调用和所有 4 个临时对象都已创建。

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


【解决方案1】:

基准谎言。他们很少衡量你想让他们做什么。在这种特殊情况下,这些方法可能是内联的,因此 __assume 只是多余的。

至于实际问题,是的,它可能会有所帮助。切换一般是通过跳转表来实现的,通过减小这个表的大小,或者去掉一些条目,编译器或许能够选择更好的CPU指令来实现switch

在您的极端情况下,它可以将switch 转换为if (i == 0) { } else { } 结构,这通常是有效的。

此外,修剪死分支有助于保持代码整洁,更少的代码意味着更好地使用 CPU 指令缓存。

然而,这些都是微优化,它们很少有回报:您需要一个分析器来指出它们,即使它们也可能难以理解要进行的特定转换(__assume 是最好的吗?) .这是专家的工作。

编辑:与 LLVM 一起行动

void foo(void);
void bar(void);

void regular(int i) {
  switch(i) {
  case 0: foo(); break;
  case 1: bar(); break;
  }
}

void optimized(int i)  {
  switch(i) {
  case 0: foo(); break;
  case 1: bar(); break;
  default: __builtin_unreachable();
  }
}

请注意,唯一的区别是 __builtin_unreachable() 的存在或不存在,这类似于 MSVC __assume(0)

define void @regular(i32 %i) nounwind uwtable {
  switch i32 %i, label %3 [
    i32 0, label %1
    i32 1, label %2
  ]

; <label>:1                                       ; preds = %0
  tail call void @foo() nounwind
  br label %3

; <label>:2                                       ; preds = %0
  tail call void @bar() nounwind
  br label %3

; <label>:3                                       ; preds = %2, %1, %0
  ret void
}

define void @optimized(i32 %i) nounwind uwtable {
  %cond = icmp eq i32 %i, 1
  br i1 %cond, label %2, label %1

; <label>:1                                       ; preds = %0
  tail call void @foo() nounwind
  br label %3

; <label>:2                                       ; preds = %0
  tail call void @bar() nounwind
  br label %3

; <label>:3                                       ; preds = %2, %1
  ret void
}

在此注意regular 中的switch 语句如何优化为optimized 中的简单比较。

这映射到以下 x86 程序集:

    .globl  regular                  |      .globl  optimized
    .align  16, 0x90                 |      .align  16, 0x90
    .type   regular,@function        |      .type   optimized,@function
regular:                             |    optimized:
.Ltmp0:                              |    .Ltmp3:
    .cfi_startproc                   |            .cfi_startproc
# BB#0:                              |    # BB#0:
    cmpl    $1, %edi                 |            cmpl    $1, %edi
    je      .LBB0_3                  |            je      .LBB1_2
# BB#1:                              |
    testl    %edi, %edi              |
    jne     .LBB0_4                  |
# BB#2:                              |    # BB#1:
    jmp     foo                      |            jmp     foo
.LBB0_3:                             |    .LBB1_2:
    jmp     bar                      |            jmp     bar
.LBB0_4:                             |
    ret                              |
.Ltmp1:                              |    .Ltmp4:
    .size   regular, .Ltmp1-regular  |      .size   optimized, .Ltmp4-optimized
.Ltmp2:                              |    .Ltmp5:
    .cfi_endproc                     |      .cfi_endproc
.Leh_func_end0:                      |    .Leh_func_end1:

注意第二种情况:

  • 代码更紧凑(指令更少)
  • 在所有路径上都有一个比较/跳转 (cmpl/je)(不是一个路径有一个跳转,一个路径有两个)

还要注意它是如此接近,以至于我不知道如何测量噪音以外的任何东西......

另一方面,它在语义上确实表明了一个意图,尽管 assert 可能更适合仅用于语义。

【讨论】:

  • Matthieu,推测编译器是否执行一种优化或另一种优化都很好,但是你能想出代码和基准来显示使用 __assume 有利于性能吗?
  • @NeilJustice:我添加了 LLVM IR 和 x86 程序集。我手头没有 Visual Studio(也不打算再拥有它),所以我改用 Clang 的 __builtin_unreachable。我没有对代码进行基准测试,JimR 的答案在这一点上似乎非常详尽,而且......无论如何,在现实世界的功能中它可能会被淹没在噪音中。
  • Matthieu,我能够确认您在 VS2010 的汇编输出中发现 __assume(0) 与 __assume(0) 之间的汇编指令差异。谢谢。
【解决方案2】:

如果您设置正确的编译器开关,似乎确实会有所不同...

接下来是三轮。没有优化,选择速度并选择大小。

本次运行没有优化

C:\temp\code>cl /EHsc /FAscu 假设.cpp Microsoft (R) 32 位 C/C++ 优化编译器版本 16.00.40219.01 用于 80x86 假设.cpp Microsoft (R) 增量链接器版本 10.00.40219.01 /出:假设.exe 假设.obj C:\temp\code>假设 NoAssumeSwitchStatement:29.5321ms 假设开关语句:31.0288 毫秒

这是最大优化 (/Ox) 请注意,/O2 在速度方面基本相同。

C:\temp\code>cl /Ox /EHsc /Fa 假设.cpp Microsoft (R) 32 位 C/C++ 优化编译器版本 16.00.40219.01 用于 80x86 假设.cpp Microsoft (R) 增量链接器版本 10.00.40219.01 /出:假设.exe 假设.obj C:\temp\code>假设 NoAssumeSwitchStatement:1.33492ms AssumeSwitchStatement:0.666948ms

这次运行是为了最小化代码空间

C:\temp\code>cl -O1 /EHsc /FAscu 假设.cpp Microsoft (R) 32 位 C/C++ 优化编译器版本 16.00.40219.01 用于 80x86 假设.cpp Microsoft (R) 增量链接器版本 10.00.40219.01 /出:假设.exe 假设.obj C:\temp\code>假设 NoAssumeSwitchStatement:5.67691ms AssumeSwitchStatement:5.36186ms

请注意,当使用速度选项时,输出的汇编代码与 Matthiu M. 所说的一致。在其他情况下调用了 switch 函数。

【讨论】:

  • Jim,我能够使用 VS2010 重现您的中等性能增益结果。 VS2008 中没有显示性能提升。谢谢。
  • 补充发现:__assume(0) switch 语句优化仅适用于 x86 VS2010 编译器,不适用于 x64 VS2010 编译器。
  • @Neil Justice:大约 5 年前,当我试图使千兆以太网饱和时,这种类型的优化对我来说很重要。我在做这件事时学到的一件事是,预取内在函数,如果使用得当,会大大加快速度。因此,如果您正在寻找这种类型的加速,请仔细查看内在函数。通过简单地将预取放在正确的位置,我在处理 128 字节字符串的哈希函数上的速度提高了 2 个数量级。我没有尝试 x64 或 VS 2008,因为此时我都无法使用。 :)
猜你喜欢
  • 2011-02-05
  • 2011-03-27
  • 2015-03-17
  • 2011-07-17
  • 1970-01-01
  • 2023-03-25
  • 2013-05-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多