【问题标题】:C 64-bit loop performance on x86x86 上的 C 64 位循环性能
【发布时间】:2014-03-06 10:14:24
【问题描述】:

对于一些使用原始套接字的 IPv4 ICMP 处理代码,我需要一个 Internet 校验和函数(一个补码校验和),但我偶然发现了在 64 位英特尔处理器(使用 gcc 4.8.2)上无法解释的行为。我想知道是否有人可以对此有所了解。

我使用 32 位累加器实现了第一个校验和函数并执行 16 位求和。然后我使用 64 位累加器和 32 位求和实现了相同的操作,认为更少的求和会导致更快的执行。结果是第一个实现的运行速度是第二个的两倍(使用 O3 优化)。我就是不知道为什么……

下面的代码实际上并没有执行准确的校验和(我已经简化了它),但说明了问题。两者都编译为 64 位,在 64 位本机平台上运行(LP64:short 16 位,int 32 位,long 64 位,指针 64 位)。

  1. 32 位累加器和 16 位求和

    unsigned short
    cksum_16_le(unsigned char* data, size_t size)
    {
        unsigned short word;
        unsigned int sum = 0;
        unsigned int i;
    
        for(i = 0; i < size - 1; i += 2)
            sum += *((unsigned short*) (data + i));
    
        sum = (sum & 0xffff) + (sum >> 16);
        sum = (sum & 0xffff) + (sum >> 16);
    
        return ~sum;
    }
    

50,000 次函数调用对相同的 10k 数据:~1.1 秒。

  1. 64 位累加器和 32 位求和

    unsigned short
    cksum_32_le(unsigned char* data, size_t size)
    {
        unsigned long word;
        unsigned long sum = 0;
        unsigned int i;
    
        for(i = 0; i < size - 3; i += 4)
            sum += *((unsigned int*) (data + i));
    
        sum = (sum & 0xffffffff) + (sum >> 32);
        sum = (sum & 0xffffffff) + (sum >> 32);
        sum = (sum & 0xffff) + (sum >> 16);
        sum = (sum & 0xffff) + (sum >> 16);
    
        return ~sum;
    }
    

50,000 次函数调用对相同的 10k 数据:~2.2 秒。

更新:

问题似乎是由硬件引起的。运行内存诊断显示偶尔的总线奇偶校验错误(不知道为什么这会比 16 位版本更影响 32 位版本,但你去了)。代码在其他服务器上按预期运行。将在接下来的几个小时内删除问题(与硬件相关,它不再特别有用)。

最终更新:

有趣的是,将 for 循环替换为 while 循环并使用 O3 优化进行编译(如下所示的 64 位累加器案例)可使 32 位和 64 位累加器案例以相同的速度运行。这是因为编译器执行了一些循环展开(奇怪的是,它不展开 for 循环)并使用 mmx 寄存器执行求和...

uint64_t sum = 0;
const uint32_t* dptr = (const uint32_t*) data;

while (size > 3)
{
    sum += (uint32_t) *dptr++;
    size -= 4;
}

【问题讨论】:

  • 你有没有试过拆开它,看看它们各自变成了什么?
  • 我们应该假设原始缓冲区在 both 示例中的段落对齐正确吗?
  • 您确定data 正确对齐了吗?如果它是 2 字节对齐的,那么 int 版本的性能会受到影响
  • 我还建议使用固定宽度的类型来避免任何关于类型大小的歧义,即uint32_tuint64_t 等。
  • 也尝试让你的函数接受unsigned int *;也许在char * 版本中,编译器决定生成不太优化的代码,因为它无法确定您是否传递了对齐的指针

标签: c performance algorithm 64-bit


【解决方案1】:

我之前也遇到过类似的问题;我在我们的任何一个代码中都找不到任何问题。但对我有用的是改变编译器。

我的猜测是 GCC 正在编写已弃用的程序集。

如果您可以反编译您的应用程序,我们可以更深入地了解这个问题,但这里没有足够的信息来继续说明。

当我反编译我的代码时,我发现它多次重写了整个方法。但这可能只适合我。

希望这对您有所帮助,在任何地方都几乎没有关于此的信息。

如果我不得不猜测我会同意 Learner,我很确定反编译的代码会指向 for 循环。我对这个问题很感兴趣,所以请回复评论。

【讨论】:

  • 请“弃用”。没有“折旧”。
【解决方案2】:

可能的答案:“i

这与 while 循环无关。当您重写 while 循环时,您也更改了迭代条件并消除了上述原因。

我也更喜欢在循环之外进行类型转换,但这也揭示了一个限制 - 你的数据必须

【讨论】:

    【解决方案3】:

    您是否使编译器的工作变得困难。在内部循环中,您通过选择索引步长和演员来自己计算字节偏移量。这可能会阻止循环展开或任何其他试图假设对齐的优化。也可能不会让编译器使用寻址模式并自行计算有效地址(或 LEA)。

    如果我这样做,我会将循环顶部的数据指针转换为您的步幅类型并将您的循环计数器增加 1。编译器可能会更快乐一些。

    【讨论】:

      【解决方案4】:

      我认为它无法展开“for”循环,因为从 char* 转换为 unsigned int *。类型转换通常会阻止编译器优化代码,因为在这种情况下无法进行完美的别名分析。如果你在循环之前先声明一个额外的本地指针来转换你的“数据”指针,这样循环中就没有任何转换,编译器应该能够优化“for”循环。

      【讨论】:

        【解决方案5】:
        sum += *((unsigned int*) (data + i)); 
        

        我不喜欢这样的演员

        64 位累加器和 32 位求和

        自从你写了:

        两者都编译为 64 位,在 64 位本机平台上运行(LP64:short 16 位,int 32 位,long >64 位,指针 64 位)。

        我建议使用 (unsigned long*)。有些人建议检查反汇编代码实际上发生了什么。我认为这是因为您的 int* 使用了长累加器。

        如果没有 O2O3 标志怎么办?能不能看一下正常编译模式下的速度是多少?

        【讨论】:

        • 程序的重点是对 32 位值求和。将强制转换更改为读取 64 位值会产生不正确的结果。
        • 那么就正常了。正如其他人评论的那样,这是填充问题。此外,如果您的整数只有 32 位大,则保存/添加两个 64 位将需要额外的操作/检查以获得正确的结果,尤其是当数字超过 32 位时。
        猜你喜欢
        • 2020-04-30
        • 1970-01-01
        • 2013-04-02
        • 2016-08-17
        • 2019-09-19
        • 2011-07-21
        • 2021-12-19
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多