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

时间:2017-11-03 06:40:39

标签: c performance gcc switch-statement

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

答案 0 :(得分:57)

您正在查看未优化的代码,因此研究它的性能并不是很有意义。如果您查看示例的优化代码,您会发现它根本不进行比较!优化程序注意到切换变量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部分的 order 不是两个示例之间的唯一变化,实际代码也不同。在其中一个案例中,案例值与a中设置的值相匹配,但在另一个中则不然。

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

答案 1 :(得分:16)

switch gcc -O2 -fverbose-asm -SCompiler optimization语句很棘手。当然,您需要启用优化(例如,尝试使用GCC使用.s编译代码,并查看生成的 .type main, @function main: .LFB0: .cfi_startproc # rsp.c:13: } xorl %eax, %eax # ret .cfi_endproc 汇编程序文件。在你的两个例子中,我在Debian / Sid / x86-64上的GCC 7简单地给出了:

switch

(因此在该生成的代码中没有switch的痕迹)

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

  

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

通常不是,如果您正在使用某些优化编译器并要求它进行优化。另请参阅this

如果这对你很重要(但它不应该对你的编译器进行微优化!),你需要进行基准测试,剖析并研究生成的汇编代码。 BTW,cache missesregister allocation可能比case - s的顺序更重要,所以我认为你根本不应该打扰。请记住最近的timing estimates近期计算机。将case置于最可读顺序中(对于处理相同源代码的下一个开发人员)。另请阅读threaded code。如果您有客观(与性能相关)的原因需要重新排序-O2 - s(这是非常不可能的,并且一生中最多只能发生一次),请写一些好的评论来解释这些原因。

如果您非常关心性能,请务必benchmarkprofile,并选择一个好的编译器并将其与相关的优化选项一起使用。也许可以尝试几种不同的optimization设置(也许是几个编译器)。您可能需要添加-march=native(除了-O3-flto -O2)。您可以考虑使用def get_session_in_flask(secret_key, cookie_str): import hashlib from itsdangerous import URLSafeTimedSerializer from flask.sessions import TaggedJSONSerializer salt = 'cookie-session' serializer = TaggedJSONSerializer() signer_kwargs = { 'key_derivation': 'hmac', 'digest_method': hashlib.sha1 } s = URLSafeTimedSerializer(secret_key, salt=salt, serializer=serializer, signer_kwargs=signer_kwargs) return s.loads(cookie_str)编译和链接以启用链接时优化等。您可能还需要profile based优化。

BTW,许多编译器都是巨大的free software项目(特别是GCCClang)。如果您非常关心性能,可以修改编译器,通过添加一些额外的优化传递(通过forking源代码,通过添加一些plugin to GCC或一些GCC MELT扩展)来扩展它。这需要数月或数年的工作(特别是要了解该编译器的内部表示和组织)。

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

答案 2 :(得分:6)

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

答案 3 :(得分:5)

switch语句通常是通过L4.2 Collection class too编译而不是简单的比较。

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

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

当案例number之间的数字差异从一种情况到另一种情况都很大时,例如case 10:case 200000:,编译器肯定不会生成跳转表,因为它应该填充大约200K条目几乎全都带有指向default:情况的指针,在这种情况下它将使用对照。

答案 4 :(得分:5)

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

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

default:

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

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

可以消除switch(x) { case 0: y++; break; ... } 非零的所有情况下的比较,但需要付出代价 x为零时的计算跳转如果x大部分时间都不为零, 这将是一个很好的交易。但是,如果x在99.9%的时间内为零,那么 可能是一个糟糕的交易。不同的编译器编写者的程度不同 他们将尝试优化前者为后者的构造。

答案 5 :(得分:4)

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

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

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

答案 6 :(得分:4)

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

在您的第一个示例中,案例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。幸运的是,编译器似乎已经按照您的预期实现了它 - 但是当您打开优化时,绝对不能指望它。