代码之家  ›  专栏  ›  技术社区  ›  msc

switch语句中事例的顺序是否会影响性能?

  •  36
  • msc  · 技术社区  · 7 年前

    我有一个 switch 案例程序:

    升序开关情况:

    int main()
    {
            int a, sc = 1;
            switch (sc)
            {
                    case 1:
                            a = 1;
                            break;
                    case 2:
                            a = 2;
                            break;
            }
    }
    

    代码汇编:

    main:
            push    rbp
            mov     rbp, rsp
            mov     DWORD PTR [rbp-4], 1
            mov     eax, DWORD PTR [rbp-4]
            cmp     eax, 1
            je      .L3
            cmp     eax, 2
            je      .L4
            jmp     .L2
    .L3:
            mov     DWORD PTR [rbp-8], 1
            jmp     .L2
    .L4:
            mov     DWORD PTR [rbp-8], 2
            nop
    .L2:
            mov     eax, 0
            pop     rbp
            ret
    

    降序开关情况:

    int main()
    {
            int a, sc = 1;
            switch (sc)
            {
                    case 2:
                            a = 1;
                            break;
                    case 1:
                            a = 2;
                            break;
            }
    }
    

    代码汇编:

    main:
            push    rbp
            mov     rbp, rsp
            mov     DWORD PTR [rbp-4], 1
            mov     eax, DWORD PTR [rbp-4]
            cmp     eax, 1
            je      .L3
            cmp     eax, 2
            jne     .L2
            mov     DWORD PTR [rbp-8], 1
            jmp     .L2
    .L3:
            mov     DWORD PTR [rbp-8], 2
            nop
    .L2:
            mov     eax, 0
            pop     rbp
            ret
    

    在这里 提升 降序 顺序

    所以 如果我有更多的开关案例,那么案例的顺序会影响性能吗?

    7 回复  |  直到 7 年前
        1
  •  58
  •   Michael Geary    7 年前

    您看到的是未优化的代码,因此研究它的性能并不是很有意义。如果你看 优化 为您的示例编写代码时,您会发现它根本无法进行比较!优化器注意到开关变量 sc 总是有价值的 1 case 2 .

    优化器还可以看到变量 a 分配后不使用,因此它会删除中的代码 case 1 还有,离开 main() 一个空函数。它删除了操作的函数prolog/epilog rbp 因为该寄存器未使用。

    因此,优化后的代码对于您的任何一个版本都是相同的 主() 功能:

    main:
        xor eax, eax
        ret
    

    简言之,对于问题中的代码,将 case

    会不会 案例 在实际生成和使用代码的更真实的示例中排序?可能不会。请注意,即使在您的 生成的代码,两个版本都测试这两个 案例 数值顺序,首先检查 1. 然后 2 ,而不考虑源代码中的顺序。显然,即使在未优化的代码中,编译器也在进行排序。

    请务必注意Glenn和Lundin的评论: 顺序 案例

    编译器使用各种策略 switch / 语句取决于使用的实际值。他们可以使用这些示例中的一系列比较,或者使用跳转表。研究生成的代码可能很有趣,但与往常一样,如果性能很重要,请注意您的优化设置和 在现实生活中。

        2
  •  18
  •   Basile Starynkevitch    7 年前

    Compiler optimization 属于 switch 陈述很复杂。当然,你需要 启用优化 (例如,尝试使用 gcc -O2 -fverbose-asm -S 具有 GCC 然后查看生成的 .s 汇编程序文件)。顺便说一句,在您的两个示例中,Debian/Sid/x86-64上的GCC 7只给出了:

            .type   main, @function
    main:
    .LFB0:
            .cfi_startproc
    # rsp.c:13: }
            xorl    %eax, %eax      #
            ret
            .cfi_endproc
    

    (因此没有任何 转换 在生成的代码中)

    如果您需要了解编译器如何优化 转换 ,有一些关于这个主题的论文,例如 this

    如果我有更多的开关案例,那么案例的顺序会影响性能?

    通常,如果您正在使用一些优化编译器并要求它进行优化。另请参见 this .

    如果这对你来说如此重要(但不应该如此,把微优化留给你的编译器!),您需要进行基准测试、评测,或许还需要研究生成的汇编代码。顺便提一下 cache misses register allocation 可能比订单更重要 case -所以我认为你一点也不应该麻烦。记住近似值 timing estimates 最近的计算机。将 案例 s最多 可读 订单(用于 下一个 开发人员正在处理相同的源代码)。另请阅读 threaded code . 如果您有客观(与绩效相关)原因需要重新订购 案例 -(这不太可能,一生中最多应该发生一次),写一些好的评论来解释这些原因。

    如果你那么在乎性能,一定要 benchmark profile ,并选择一个好的编译器,将其与相关优化选项一起使用。也许实验几种不同的 optimization 设置(可能还有几个编译器)。您可能需要添加 -march=native (除了 -O2 -O3 ). 你可以考虑编译 具有 -flto -O2 要启用链接时间优化等,您可能还需要 profile based 优化。

    顺便说一句,许多编译器都很庞大 free software 项目(尤其是 通用条款 Clang ). 如果您非常关心性能,可以修补编译器,通过添加一些额外的优化过程(通过 forking 源代码,通过添加一些 plugin to GCC 或者一些 GCC MELT 扩展)。这需要数月或数年的工作(尤其是理解编译器的内部表示和组织)。

    (别忘了考虑开发成本;在大多数情况下,开发成本要高得多)

        3
  •  6
  •   Thilo    7 年前

    性能主要取决于给定数据集的分支未命中数,而不是案例总数。而这又在很大程度上取决于实际数据以及编译器如何选择实现切换(调度表、链式条件、条件树——不确定是否可以从C中控制)。

        4
  •  6
  •   supercat    7 年前

    case 0:; case 1:; case 2:; case 3:; default: 可能导致编译器将操作数与12进行比较,如果更少,则使用12项跳转表。省略这些情况可能会导致编译器在将差异与8进行比较之前减去4,然后使用8项表。

    试图优化switch语句的一个困难是,编译器通常比程序员更清楚在给定某些输入时不同方法的性能如何变化,但程序员可能比编译器更清楚程序将接收到的输入分布。假设如下:

    if (x==0)
      y++;
    else switch(x)
    {
      ...
    }
    

    “智能”编译器可能会认识到将代码更改为:

    switch(x)
    {
      case 0:
        y++;
        break;
      ...
    }
    

    可以消除所有情况下的比较 x 是非零的,代价是 计算跳转的 x 为零。如果 x 在大多数情况下为非零, 那将是一笔不错的交易。如果 x 在99.9%的时间里是零 这可能是一笔糟糕的交易。不同的编译器编写器在以下方面有所不同: 他们将尝试将前者优化为后者。

        5
  •  5
  •   alinsoar    7 年前

    switch语句通常通过以下方式编译: jump tables 不是通过简单的比较。

    因此,如果您排列case语句,则不会损失性能。

    然而,有时将更多案例保持连续顺序,并且在某些条目中不使用中断/返回是有用的,以便执行流转到下一个案例,并避免重复代码。

    当数字之间的差异出现时 number 从一个案例到另一个案例都很大,例如 case 10: case 200000: 编译器肯定不会生成跳转表,因为它应该用指向 default: 在这种情况下,它将使用比较。

        6
  •  4
  •   Graham    7 年前

    你的问题很简单-你的代码不一样,所以它不会生成相同的程序集!优化的代码不仅取决于单个语句,还取决于它周围的一切。在这种情况下,很容易解释优化。

    在第一个示例中,案例1的结果是a=1,案例2的结果是a=2。编译器可以对此进行优化,为这两种情况设置a=sc,这是一条语句。

    如果你简单地拿第一个例子来交换案例的顺序 那么你应该得到相同的汇编程序。

    int main()
    {
        int a, sc = 1;
    
        switch (sc)
        {
            case 1:
            case 2:
                a = sc;
                break;
        }
    }
    

    这也应该给出完全相同的汇编程序。

    顺便提一下,测试代码假设sc实际上已被读取。大多数现代优化编译器都能够发现sc在赋值和switch语句之间没有变化,并用常量值1代替读取sc。进一步优化将删除switch语句的冗余分支,然后甚至可以优化分配,因为a实际上没有改变。从变量a的角度来看,编译器还可能发现a没有在别处读取,因此将该变量从代码中完全删除。

    如果您真的想读取sc并设置a,则需要同时声明它们 volatile . 幸运的是,编译器似乎以您期望的方式实现了它-但当您打开优化时,您绝对不能期望这样。

        7
  •  4
  •   user8880145    7 年前

    在比较汇编代码之前,您可能应该启用编译器的优化,但是问题是,您的变量在编译时是已知的,因此编译器可以删除函数中的所有内容,因为它没有任何副作用。

    This example 这表明,即使在示例中更改switch语句中事例的顺序,如果启用了优化,GCC和大多数其他编译器也会对其重新排序。 我使用了extern函数来确保只有在运行时才知道这些值,但我也可以使用 rand 例如

    此外,当您添加更多案例时,编译器可能会用一个包含函数地址的表来替换条件跳转,并且它仍然会被GCC重新排序,如图所示 here .