了解C x86小功能的优化

问题描述

我的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);
}

通过提交指向单个uint16_t整数的低16位和高16位的uint32_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,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,(%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,%edx
  movzwl    (%rdi),%eax
  subl  %edx,(%rdi)
  movzwl    (%rdi),%eax
  movzwl    (%rdi),%edx
  sall  $7,%eax
  shrw  $9,(%rdi)
  ret
  .cfi_endproc

我不明白为什么函数的行为会有所不同。

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

解决方法

主要问题不太可能出在函数a_inverse()本身,并且volatile限定参数的事实会改变行为,这是偶然的情况,如果数据实际上不是不稳定的。

很可能,问题的根源实际上在呼叫者中。确实,在注释中,您描述了如何计算参数指针,如下所示:

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;
}

...,并希望它至少与之一样快,尤其是因为没有混淆风险会限制可用的优化。