【问题标题】:reducing if statements in C for efficiency减少 C 中的 if 语句以提高效率
【发布时间】:2018-08-04 17:17:14
【问题描述】:

我正试图通过减少条件语句的数量来提高我的代码效率,因为我将在未来以汇编形式编写代码。你们有什么方法可以减少语句以提高效率?

if (j==18)
{
    store=12;
}
else if(j==30)
{
    store=10;
}
else if(j==42)
{
    store=8;
}
else if(j==54)
{
    store=6;
}
else if(j==66)
{
    store=4;
}
else if(j==78)
{
    store=2;
}
else if(j==92)
{
    store=2;
}
else if(j==106)
{
    store=4;
}
else if(j==120)
{
    store=6;
}
else if(j==134)
{
    store=8;
}
else if(j==148)
{
    store=10;
}
else if(j==162)
{
    store=12;
}
else store=1;

【问题讨论】:

  • 为什么如果,你为什么不计算呢?在我看来,如果足够的话,就可以了,然后除以 12 或 14,根据需要四舍五入,然后加/减以获得最终值。什么是jstore,整数?多少位?对于您未显示的其他值应该发生什么(我看到 now store=1; 最后,这是硬性要求吗?)
  • 嗯,为了减少if语句的数量,你必须有一个清晰的模式,然后才能使用算术运算。
  • 为了可惜,为代码选择一致的缩进方案。在示例中到处都是。您将多久执行一次此代码?映射值列表多久更改一次?输入值的范围是多少?输出是否严格限制为 1..12?除了商店 1,您所有的商店都有两个映射到它们的代码。这是巧合吗?
  • 基本上,这段代码在插入排序的一个版本中运行,该版本正在通过菱形,所以我必须跳过某些索引,以便在 while 循环中使用 store 变量来确定递减插入排序
  • 过多地依赖于实际数据,无法在所有情况下都有正确的答案。对于这个特定的数据,我会使用一个计算的查找表。

标签: c if-statement assembly conditional reduce


【解决方案1】:
if (j < 18 || 162 < j) {
    store = 1;
} else if (j < 90) {
    int mod12 = (j-6) % 12;
    // ((j-6)/12) -> 18=>1, .. 78=>6  (/6 used to get *2)
    store = (mod12) ? 1 : 14 - ((j-6) / 6);
} else {
    int mod14 = (j-8) % 14;
    // ((j-8)/14) -> 92=>6, ... 162=>11 (/7 used to get *2)
    store = (mod14) ? 1 : ((j-8) / 7) - 10;
}

这可以通过手动汇编器直接实现,尽管现代 C++ 编译器会比普通人做得更好,for example gcc 7.3 produces something a bit better 比我最初想象的要好(而且我不想手写)。


其实gcc可以是hand-holded a bit to understand that formula better

修改来源:

if (j < 18 || 162 < j) {
    store = 1;
} else if (j < 90) {
    int mod12 = (j-6) % 12;
    // ((j-6)/12) -> 18=>1, .. 78=>6
    store = (mod12) ? 1 : 14 - 2*((j-6) / 12);
} else {
    int mod14 = (j-8) % 14;
    // ((j-8)/14) -> 92=>6, ... 162=>11
    store = (mod14) ? 1 : 2*((j-8) / 14) - 10;
}

为了完整起见,这里是switch 版本(没有对其进行基准测试,但应该比上面的代码慢很多):https://godbolt.org/g/ELNCYD

看起来 gcc 无法解决这个问题,并且确实为此使用了许多“ifs”。


:所以在检查了所有这些编译器/和 cmets 之后,这看起来是最 [性能] 最优 x86_64 解决方案(子例程在 edi 中采用“j”并在 @ 中返回“store” 987654330@)(NASM 语法):

; input: edi = j, output: rax = store
storeByJ:
    sub     edi, 18
    cmp     edi, (162-18)
    ja      .JoutOfRange    ; j < 18 || 162 < j -> return 1
    ; rdi = 0 .. 162-18 = 144
    movzx   eax, byte [rel .JtoStoreLUT + rdi]
    ret
.JoutOfRange:
    mov     eax,1
    ret

.JtoStoreLUT:
    ;        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
    db      12, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1         ; 18->12
    db      10, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1         ; 30->10
    db       8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1         ; 42->8
    db       6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1         ; 54->6
    db       4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1         ; 66->4
    db       2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1   ; 78->2
    db       2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1   ; 92->2
    db       4, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1   ;106->4
    db       6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1   ;120->6
    db       8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1   ;134->8
    db      10, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1   ;148->10
    db      12                                          ;162->12

如果您有更大的范围(更多值)仍然是 +12 并且在某个点 +14 微分之后,公式可能会变得更好(通过从太大的 LUT(查找表)中保存缓存内存),但此时这段代码的长度约为 170B,即使有 LUT 数据,所以它很可能适合整个循环使用它。


编辑:另一种变体,具有一半大小的 LUT,但使用 ror 在范围测试中通过单次跳转过滤掉奇数值(我不确定性能,但与任何其他代码效率问题一样,分析是绝对必要的,尤其是理论推理,如果你不能在基准测试中确认你的理论,修复你的基准测试(很有可能),或者找出 CPU 内部的惊人复杂性以及你是如何误解的......(但这仍然很可能经常发生))..并且使用cmovCC(总共97B)消除了范围分支:

; input: edi = j, output: eax = store
storeByJ:
    mov     eax, 1
    sub     edi, 18
    ror     edi, 1          ; /2 but keeps "odd" bit in the edi
                            ; to make it fail range check on next line
    cmp     edi, (162-18)/2
    cmova   edi, eax        ; j < 18 || 162 < j || j&1 -> return 1 (from LUT[1])
    ; rdi = 0 .. (162-18)/2 = 72  # rdi = (j-18)/2
    movzx   eax, byte [rel .JtoStoreLUT + rdi]
    ret

.JtoStoreLUT:
    ;        0, 1, 2, 3, 4, 5, 6
    db      12, 1, 1, 1, 1, 1       ;  18->12
    db      10, 1, 1, 1, 1, 1       ;  30->10
    db       8, 1, 1, 1, 1, 1       ;  42->8
    db       6, 1, 1, 1, 1, 1       ;  54->6
    db       4, 1, 1, 1, 1, 1       ;  66->4
    db       2, 1, 1, 1, 1, 1, 1    ;  78->2
    db       2, 1, 1, 1, 1, 1, 1    ;  92->2
    db       4, 1, 1, 1, 1, 1, 1    ; 106->2
    db       6, 1, 1, 1, 1, 1, 1    ; 120->2
    db       8, 1, 1, 1, 1, 1, 1    ; 134->2
    db      10, 1, 1, 1, 1, 1, 1    ; 148->2
    db      12                      ; 162->2

【讨论】:

  • 其他编译器使用跳转表进行切换,因此非常糟糕 (godbolt.org/g/nx9XK2)。 比以j&gt;&gt;1 为索引的movzx 字节LUT 差。在j 分支为奇数或超出范围后,我肯定会在这里考虑 LUT。我猜我应该将此报告为错过的优化。
  • 看起来 gcc 仍在使用样板签名除法代码,即使在范围检查证明 j-6j-8 是非负数之后:( 在正确的地方使用 unsigned ju = j 可以避免godbolt.org/g/pBLp44。不过,如果这是在热循环中,那么字节 LUT 仍然要好得多。它只需要几个缓存行。
  • 是的,我认为修复这个错过的优化可能会使大量代码受益(特别是如果使用 retpolines 编译间接分支作为 Spectre 缓解)。我有点惊讶没有编译器实现它。
  • @PeterCordes 所以我用字节 LUT 编写了 x86_64 变体,如果你敢联系编译器供应商,请随意重用,我对此没有足够的热情(我有点高兴仍然有手工组装得到回报的地方;))。顺便说一句,shr edi,1 + jc 获得一半 LUT 大小值得吗?
  • @Ped7g:哦,对了,忘了必须测试奇数并返回默认值。是的,ror 的想法非常好。顺便说一句,您可以通过使用带有选择默认索引的 cmov 来使其无分支。如果 rorrorx 意味着分支不再完美预测,也许值得这样做。
【解决方案2】:

嗯,使用 switch 语句而不是一系列 if 语句肯定会更快(并且更紧凑)。大多数语言可以比一系列 if 更好地优化 switch 语句。你的代码在 switch 语句形式中看起来像这样:

switch(j) {
    case 18:
        store = 12;
        break;
    case 30:
        store = 10;
        break;
    // and so on
    default:
        store = 1;
}

当然,这不会像没有条件的代码版本那样快(可能)。如果你能想出一个数学公式来计算store 的值,用j 表示,那(可能)会快很多。

【讨论】:

  • 我将如何使用此方法实现最终的 else 语句以便 store=1?抱歉,我对编程很陌生,这只是我编程的第二个月,对于基本问题感到抱歉。
  • 这可能会编译为跳转表(如果j 值不是太稀疏)或比较/分支指令链。制作数据查找表可能会更好,因为 AFAIK 编译器通常不会进行这种优化。
  • 是的,您可能是对的,数据查找表将是最快的选择。不幸的是,我在写答案时没有想到。
【解决方案3】:

如果这些都是您必须处理的 j,我会尝试使用 162 字符甚至更少的数组。 这听起来像是一种内存浪费,但考虑到所有这些指令都被保存了,它们也占用了内存。 M2c

【讨论】:

  • 所有j 值都是偶数,因此如果它在范围内且偶数,您可以右移它。例如if (j &amp; 1 || j&lt;min || j&gt;max ) store = 1; else store = LUT[ j&gt;&gt;1 ]; 此外,您可以减去最小值,因此第一个有用的表条目位于 [0]。作为编译范围检查的一部分,编译器无论如何都会想要sub reg, 10
  • 这也是我在写“162 char or even less”时的想法。
猜你喜欢
  • 2016-02-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-16
  • 2012-05-11
  • 2011-02-02
  • 2019-06-06
相关资源
最近更新 更多