【问题标题】:Understanding C x86 optimization of a small function了解C x86优化一个小函数
【发布时间】:2020-09-11 00:03:56
【问题描述】:

我有一个相对较小的 C 函数。

#define ROTL16(x, r) (((x) << (r)) | (x >> (16 - (r))))

void a_inverse(uint16_t *left, uint16_t *right) {
  *right ^= *left;
  *right = ROTL16(*right, 14);
  *left -= *right;
  *left = ROTL16(*left, 7);
}

通过提交指向单个 uint32_t 整数的低 16 位和高 16 位的 uint16_t 指针调用该函数。

uint32_t whole;
uint16_t * left = (uint16_t *)&whole;
uint16_t * right = left + 1;

a_inverse(left, right);

但该功能未按预期执行。 所以我一味加了volatile关键字。

#define ROTL16(x, r) (((x) << (r)) | (x >> (16 - (r))))

void a_inverse(volatile uint16_t *left, volatile uint16_t *right) {
  *right ^= *left;
  *right = ROTL16(*right, 14);
  *left -= *right;
  *left = ROTL16(*left, 7);
}

这次它可以正常工作了。

将输出程序集与 -O3 进行比较,我没有发现任何问题

这是一个非易失性版本

a_inverse:
  .cfi_startproc
  endbr64
  movzwl    (%rsi), %ea
  xorw  (%rdi), %ax
  rorw  $2, %ax
  movw  %ax, (%rsi)
  movzwl    (%rdi), %edx
  subl  %eax, %edx
  movl  %edx, %eax
  rolw  $7, %ax
  movw  %ax, (%rdi)
  ret
  .cfi_endproc

这是带有volatile关键字的那个

a_inverse:
  .cfi_startproc
  endbr64
  movzwl    (%rdi), %edx
  movzwl    (%rsi), %eax
  xorl  %edx, %eax
  movw  %ax, (%rsi)
  movzwl    (%rsi), %eax
  movzwl    (%rsi), %edx
  sall  $14, %eax
  shrw  $2, %dx
  orl   %edx, %eax
  movw  %ax, (%rsi)
  movzwl    (%rsi), %edx
  movzwl    (%rdi), %eax
  subl  %edx, %eax
  movw  %ax, (%rdi)
  movzwl    (%rdi), %eax
  movzwl    (%rdi), %edx
  sall  $7, %eax
  shrw  $9, %dx
  orl   %edx, %eax
  movw  %ax, (%rdi)
  ret
  .cfi_endproc

而且我不明白为什么这些函数的行为不同。

你能帮我理解可能是什么原因吗?
预期/意外结果的唯一区别是那些 volatile 用法。 删除任一volatile 都会导致意外结果。

【问题讨论】:

  • 您是如何确定“功能未按预期执行”的?
  • 您确定leftright 是相邻的并且不会意外重叠吗?你如何传递输入?确保+ 1 不是字节。
  • 那么你有一个严格的混叠违规,结果是未定义的行为。这可能会或可能不会解释差异,但由于您使用的是 GCC,您可以尝试使用 -fno-strict-aliasing 编译第一个版本,看看它是否会有所不同。
  • 我倾向于猜测问题实际上出在调用者身上。编译器(没有-fno-strict-aliasing)可能会根据类型分析假设对a_inverse() 的调用无法修改whole 的值,并根据正确性执行优化。
  • 您需要提供一个完整的程序来重现您所看到的结果,特别是minimal reproducible example,然后任何人才能正确回答您的问题。

标签: c assembly gcc x86 strict-aliasing


【解决方案1】:

主要问题不太可能出在函数 a_inverse() 本身,如果数据实际上不是易失性的,volatile-qualifying 参数改变了行为这一事实是偶然的。

问题的根源很可能实际上在调用者身上。实际上,在 cmets 中,您描述计算参数指针的方式如下:

uint32_t *whole = ...;
uint16_t *left = (uint16_t *) whole;
uint16_t *right = left + 1;

这大概会被一个电话跟进,例如

a_inverse(left, right);

。允许类型转换为uint16_t *。指针算法的允许性是有争议的,但在实践中它不太可能成为问题。但是由于生成的leftright 指针实际上并不指向uint16_t 对象,因此试图使用它们来访问它们所指向的对象违反了严格的别名规则(C18,第6.5/7 段),并且因此会产生未定义的行为。

严格的别名规则表明,只能通过兼容类型的左值 +/- 限定和符号差异,可能在联合中,或通过字符类型的左值访问对象。 (后者的主要含义是您可以通过char * 访问任何对象的表示。)这允许编译器使用数据类型分析来通知优化选择。

GCC 有一个标志-fno-strict-aliasing,它告诉它不要使用类型分析来做出优化决策——基本上,它迫使它假设任何指针都可能别名为任何其他指针。如果您不想解决问题,那么使用该标志可能会缓解它。

另一方面,我怀疑所有指针(错误)使用都是错误的优化尝试。我会重写函数而不:

uint32_t a_inverse(uint32_t whole) {
    uint16_t left = whole;  // x86 is little-endian
    uint16_t right = whole >> 16;

    right ^= left;
    right  = ROTL16(right, 14);
    left  -= right;
    left   = ROTL16(left, 7);

    return right << 16 + left;
}

...并期望它至少一样快,尤其是因为不存在限制可用优化的混叠风险。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-29
    • 1970-01-01
    • 2014-10-12
    • 2018-01-19
    • 2019-07-29
    • 1970-01-01
    相关资源
    最近更新 更多