【问题标题】:Is worth to use mod to replace if statement for circle index是否值得使用 mod 替换循环索引的 if 语句
【发布时间】:2016-06-07 10:13:26
【问题描述】:

我需要一个变量来指向数组索引,当它到达数组末尾时,就像圆一样返回 0。我知道我可以使用if语句来判断,但是我不确定使用mod来实现相同的功能是否会更快,谁能给我一些建议?

int p=0;
int arr[10];
void add_index(){   
   if(p==9) p=0;
   else     p++;
}

或者

int p=0;
int arr[10];
void add_index(){
   p=(p+1)%10;
}

【问题讨论】:

  • 检查生成的程序集以开始比较...
  • 这取决于您的编译器和程序运行的硬件。你为什么不直接测量一下?
  • 如果你真的在函数调用中拥有它,那么时间差异甚至不应该是可测量的。无论如何,您的代码看起来不够复杂,因此这会有所作为。如果您首先专注于编写好的代码,那就更好了:没有全局变量,没有() 参数列表,没有魔术常量。

标签: c arrays indexing geometry mod


【解决方案1】:

曾几何时,绝对是的。这些天,可能没有!

我将以英特尔 Skylake 为例。 DIV 指令(产生商和余数,用于此类事情)在 32 位被除数和除数上运行,具有 23 个周期的延迟和 6 个周期的倒数吞吐量。也就是说,根据它与其他操作的流水线方式,“成本”是 6-23 个周期。 (好吧,由于执行端口,它比那个复杂一点,但在这里和我一起工作。)正确预测的跳转是 0.5-2 个周期,具体取决于它是否被采取,错误预测的跳转有 16 个惩罚-17 个周期。 (所有人都向 Agner Fog 致敬。)

英特尔分支预测硬件真的非常好。期望它正确预测每第九个分支都会被采用可能是太过分了,但在一个内部循环中,我至少希望它能够正确预测其他 8 次。这意味着 if 语句平均大约需要 3.5 个周期(不计算各种整数操作,这会增加 可能 1-2 个周期)。哦,那是假设编译器是特别是 derpy,而不是像它应该的那样使用 CMOV。

要记住的是,整数除法是现代 CPU 可以做的最慢的“正常”事情之一。但是,对于已知除数的模数,您可以改为使用特殊的加法/乘法/移位序列。因此,在上述代码的情况下,除数是编译时常量而不是取自变量,您实际上可能会击败 DIV。这些序列可能很难流水线化,所以很难说它是否真的会赢。无论如何,现代编译器绝对知道这样的技巧。

底线:很难说。如果您在内部循环中执行该操作 huge 次,实际上可能值得尝试两种方式和时间。但是,您可能不会看到有意义的差异,也不会证明在其上花费优化时间是合理的。但是我经常写代码,需要极高的性能,我以前默认为 PPC 的模数,现在我默认为 x64 的 if/else。 (嗯,三元。)

【讨论】:

  • @Vorac 是的,我知道。请参阅我的倒数第二段。
【解决方案2】:

我写了一个小测试,用gcc -O4优化编译。

这是来自此测试的 add_index_modadd_index_if 实现:

void add_index_mod(int *p) {
    *p = (*p + 1) % 10;
}

void add_index_if(int *p) {
    if (*p == 9)
        *p = 0;
    else
        (*p)++;
}

这就是我得到的add_index_mod

mov eax, dword [rdi]
mov edx, 0x66666667
lea ecx, dword [rax + 1]
mov eax, ecx
imul edx
mov eax, ecx
sar eax, 0x1f
sar edx, 2
sub edx, eax
lea eax, dword [rdx + rdx*4]
add eax, eax
sub ecx, eax
mov dword [rdi], ecx
ret

在这里我们可以看到编译器将 div 替换为 mul、shifts 和 subs 的序列。这个技巧很好描述here

这就是我得到的add_index_if

mov edx, dword [rdi]            
lea eax, dword [rdx + 1]        
cmp edx, 9                      
mov edx, 0                      
cmove eax, edx                  
mov dword [rdi], eax            
ret

这里没有什么特别的,只是 cmp 和条件 mov。

所以现在你可以尝试计算这两者的汇编代码的效率 使用此table 的函数。但这不是最好的方法,因为乱序执行、分支预测等。

所以正如我上面提到的,我只是写了一个小测试:

#include <stdio.h>
#include <stdint.h>

#define REPEATS (1 << 30)

static inline uint64_t rdtsc() {
  unsigned int hi, lo;
  __asm__ volatile("rdtsc" : "=a" (lo), "=d" (hi));
  return ((uint64_t)hi << 32) | lo;
}

void add_index_mod(int *p) {
    *p = (*p + 1) % 10;
}

void add_index_if(int *p) {
    if (*p == 9)
        *p = 0;
    else
        (*p)++;
}

int main() {
    int p = 0;
    uint32_t i;
    uint64_t start, stop;
    double delta, ticks_per_call;

    // mod ================================

    start = rdtsc();

    for (i = 0; i < REPEATS; ++i) {
        add_index_mod(&p);
    }

    stop = rdtsc();

    // gcc with -O4 can remove above loop
    // if we don't use its result so print it
    printf("%d\n", p);

    delta = (double)(stop - start);
    ticks_per_call = delta / REPEATS;
    printf("add_index_mod: %f\n", ticks_per_call);


    // if ================================

    start = rdtsc();

    for (i = 0; i < REPEATS; ++i) {
        add_index_if(&p);
    }

    stop = rdtsc();

    printf("%d\n", p);

    delta = (double)(stop - start);
    ticks_per_call = delta / REPEATS;
    printf("add_index_if: %f\n", ticks_per_call);

    return 0;
}

这是我的英特尔酷睿 i5-6500 的输出:

add_index_mod: 9.643092
add_index_if: 2.063125

因此,在我的 CPU 上,add_index_if 的大量调用比 add_index_mod 快 5 倍。

【讨论】:

    【解决方案3】:

    我宁愿使用 mod,而不深入研究情况,这里有几件事需要考虑。

    1) 当您分支时(if 语句/函数调用/等),您的处理器可能需要刷新它的管道。这意味着,您有一堆指令在知道它们是否需要执行之前已经执行,而“处理能力”只是丢失了。我并不是说这会总是发生,但它可以

    2) 假设您想找到在当前条目之前 5 个条目发生的条目,并对其进行一些数学运算。假设您需要两者之间的平均值。您可以有一个更优雅的解决方案,而不是进行数学运算和存储结果、拥有一个额外的变量以及所有这些笨拙的方法。

    (array[index%10] + array[(index-5)%10])/2;
    

    这现在可以环绕您的循环缓冲区。

    如果你这样做,我觉得你会更习惯用这种方式编写代码,而不是用 if/else 语句来确定你的索引。

    不过,有一点需要注意。如果取负数的模,c 在数学上是错误的。你最终会得到否定的答案。因此,如果您要执行此类操作(例如,在当前条目之前查找条目),请从顶部索引开始索引。

    希望这会有所帮助。

    【讨论】:

      猜你喜欢
      • 2016-08-03
      • 2014-03-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-11
      • 2017-03-06
      相关资源
      最近更新 更多