繁体   English   中英

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

[英]Does the order of cases in a switch statement affect performance?

我有一个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

这里, 升序的情况产生了更多的汇编而不是降序

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

您正在研究未经优化的代码,因此研究它的性能并不是很有意义。 如果您查看示例的优化代码,您会发现它根本不进行比较! 优化器注意到switch变量sc总是具有值1 ,因此它删除了无法访问的case 2

优化器还会看到变量a在分配后未使用,因此它也删除了case 1中的代码,将main()保留为空函数。 并且它删除了操作rbp的函数prolog / epilog,因为该寄存器未使用。

因此,对于main()函数的任一版本,优化代码的结果都相同:

main:
    xor eax, eax
    ret

简而言之,对于问题中的代码,将case语句放在哪个顺序并不重要,因为根本不会生成任何代码。

在一个更真实的例子中, case命令是否重要,其中代码实际生成和使用? 可能不是。 请注意,即使在未经优化的生成代码中,两个版本都会按数字顺序测试两个case值,首先检查1然后检查2 ,无论源代码中的顺序如何。 很明显,即使在未经优化的代码中,编译器也在进行一些排序。

请务必注意Glenn和Lundin的评论: case部分的顺序不是两个例子之间的唯一变化,实际代码也不同。 在其中一个中,case值与设置为a值匹配,但在另一个中不匹配。

编译器根据使用的实际值使用各种switch / case语句策略。 他们可以使用这些示例中的一系列比较,或者可能使用跳转表。 研究生成的代码可能很有意思,但与往常一样,如果性能很重要,请观察优化设置并在现实情况下对其进行测试

编译器优化 switch语句很棘手。 当然,您需要启用优化 (例如,尝试使用gcc -O2 -fverbose-asm -S使用GCC编译代码,并查看生成的.s汇编程序文件)。 BTW在你的两个例子中我在Debian / Sid / x86-64上的GCC 7简单地给出了:

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

(所以在那个生成的代码中没有switch痕迹)

如果您需要了解编译器如何优化switch ,那么有一些关于该主题的论文,例如本文

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

不是一般的,如果你使用的是一些优化的编译器,并要求其进行优化。 另请参见

如果这对你很重要(但它不应该给你的编译器留下微优化!),你需要进行基准测试,剖析并研究生成的汇编代码。 顺便说一句, 缓存未命中寄存器分配可能比case -s的顺序更重要,所以我认为你根本不应该打扰。 请记住最近计算机的大致时序估计 以最可读的顺序放置case (对于处理相同源代码的下一个开发人员)。 阅读有关线程代码的信息 如果您有客观(与绩效相关)的理由来重新订购case (这是非常不可能的,并且在您的有生之年最多只能发生一次),请写一些好的评论来解释这些原因。

如果您非常关心性能,请务必进行基准测试分析 ,并选择一个好的编译器并将其与相关的优化选项一起使用。 也许可以尝试几种不同的优化设置(也许是几种编译器)。 您可能需要添加-march=native (除-O2-O3 )。 您可以考虑使用-flto -O2进行编译和链接以启用链接时优化等。您可能还需要基于配置文件的优化。

顺便说一下,许多编译器都是巨大的自由软件项目(特别是GCCClang )。 如果您非常关心性能,可以修补编译器,通过添加一些额外的优化传递来扩展它(通过向源代码分配 ,通过向GCC或某些GCC MELT扩展添加一些插件)来扩展它。 这需要数月或数年的工作(特别是要了解该编译器的内部表示和组织)。

(不要忘记考虑开发成本;在大多数情况下,它们的成本要高得多)

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

switch语句通常是通过跳转表而不是简单的comparaisons编译的。

因此,如果您置换案例陈述,则性能不会有任何损失。

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

当数字之间的情况下,差别number是很大的,从一个案件到另一个,这样的case 10:case 200000:编译器肯定不会产生跳表,因为它应该朝向指针填补约20万项,几乎所有的default: case,在这种情况下,它将使用comparaisons。

在大多数案例标签是连续的情况下,编译器通常会处理switch语句以使用跳转表而不是比较。 编译器决定使用何种形式的计算跳转(如果有的话)的确切方法将因不同的实现而异。 有时在switch语句中添加额外的case可以通过简化编译器生成的代码来提高性能(例如,如果代码使用案例4-11,而案例0-3以默认方式处理,则添加显式case 0:; case 1:; case 2:; case 3:; default:之前default:可能导致编译器将操作数与12进行比较,如果更少,则使用12项跳转表。省略这些情况可能会导致编译器在比较差值之前减去4 8,然后使用8项表。

尝试优化switch语句的一个难点是编译器通常比程序员更了解在给定某些输入时不同方法的性能如何变化,但程序员可能比编译器更了解程序将接收的输入分布。 给出如下内容:

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

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

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

可以消除x在非零的所有情况下的比较,以x为零时计算的跳跃为代价。 如果x在大多数时间都不为零,那将是一个很好的交易。 但是,如果x在99.9%的时间内为零,则可能是一个糟糕的交易。 不同的编译器编写者在尝试优化前者到后者的构造方面的程度不同。

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

此示例显示即使您在示例中更改switch语句中的个案的顺序,GCC和大多数其他编译器也会在启用优化时对它们重新排序。 我使用extern函数来确保值只在运行时知道,但我也可以使用rand作为例子。

此外,当你添加更多的情况下,编译器可以替代由包含的函数地址表的条件跳转,它仍将获得由GCC重新排序可以看出这里

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

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

在第二个示例中,案例1导致a = 2,案例2导致a = 1。 编译器不能再使用该快捷方式,因此必须为两种情况明确设置a = 1或a = 2。 当然这需要更多的代码。

如果您只是采用第一个示例并交换了案例和条件代码的顺序,那么您应该获得相同的汇编程序。

您可以使用代码测试此优化

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和设置sc,你需要声明它们都是volatile 幸运的是,编译器似乎已经按照您的预期实现了它 - 但是当您打开优化时,绝对不能指望它。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM