32bit_me (32bit_me) wrote,
32bit_me
32bit_me

Category:

Лекция по aliasing

Небольшая лекция по проблемам оптимизации кода, связанным с aliasing (синонимами).



Также к данной теме: https://stackoverflow.com/questions/1225741/performance-impact-of-fno-strict-aliasing

Но на самом деле не всё так страшно. В качестве примера рассматривается функция, которая инвертирует значение float путём изменения знакового разряда:



Утверждается, что так делать нельзя, и компилятор превратит этот код в нечто нерабочее:



Здесь мы видим, что компилятор вставил команду fld (помещает переменную в стек fpu), т.к. согласно calling convention так производится возврат float. Ошибкой здесь является то, что операция xor происходит после fld, т.к. gcc считает эти операции независимыми. Он делает это из-за опции -fstrict-aliasing, которая заставляет его считать независимыми все указатели, не соответствующие правилу Strict aliasing (подробнее можно почитать, например, здесь).

Однако, мне кажется, что не всё так страшно.
Напишем код:

float invert(float value) {
 int* const raw = (int*) &value;
 *raw ^= (1 << 31);
 return *(float*) raw;
}


Начнем с LLVM версии 5.0.0. Скомпилируем LLVM IR в clang с -O1 (с любой оптимизацией больше -О0 будет то же самое):
define float @invert(float %value) local_unnamed_addr #0 {
entry:
  %0 = bitcast float %value to i32
  %xor = xor i32 %0, -2147483648
  %1 = bitcast i32 %xor to float
  ret float %1
}


Мы видим, что всё правильно, как и должно быть. Инвертируется старший разряд.

Можно скомпилировать в ассемблер x86-64:
.LCPI0_0:
  .long 2147483648 # float -0
  .long 2147483648 # float -0
  .long 2147483648 # float -0
  .long 2147483648 # float -0
invert(float): # @invert(float)
  xorps xmm0, xmmword ptr [rip + .LCPI0_0]
  ret


Непонятно, зачем константа вставлена 4 раза, но в целом всё верно.

Теперь попробуем gcc c -O2:
invert(float):
  movd eax, xmm0
  add eax, -2147483648
  mov DWORD PTR [rsp-4], eax
  movss xmm0, DWORD PTR [rsp-4]
  ret

Более замысловато, но результат будет тот же. И с -fstrict-aliasing будет то же самое. И только если в опциях будет -m32, у нас появится команда fld. Clang тоже вставляет fld (или flds) при опции -m32. Опция -m32 заставляет эти компиляторы генерировать 32-битный код i386 (если быть точнее, то clang выдаёт следующие атрибуты в IR-коде: "target-cpu" = "pentium4" "target-features" = "+fxsr, +mmx, +sse, +sse2, +x87").
gcc4.4.7 делает ошибку в коде, которую мы видим на слайде в начале, и с опцией -fstrict-aliasing, и без неё. Но, начиная с gcc4.5.3, эта ошибка исчезает. Например, gcc7.2 генерирует следующий код:
invert(float):
  sub esp, 4
  mov eax, DWORD PTR [esp+8]
  add eax, -2147483648
  mov DWORD PTR [esp], eax
  fld DWORD PTR [esp]
  add esp, 4
  ret

Этот код генерируется для опции -m32 и при -fstrict-aliasing, и без него.

А что на других платформах?
Всё нормально. Компилятор Clang, ядро ARM (32 бита):
invert:
	.fnstart
@ BB#0:                                 @ %entry
	eor	r0, r0, #-2147483648
	bx	lr

Компилятор Clang, ядро AARCH64 (ARM 64 бита):
invert:                                 // @invert
// BB#0:                                // %entry
	fneg	s0, s0
	ret

Обратите внимание, компилятор автоматически распознал операцию изменения знака float!
GCC 6.3 (ARM 32)
invert(float):
  add r0, r0, #-2147483648
  bx lr


GCC 6.3 (ARM 64):
invert(float):
  fmov w0, s0
  eor w0, w0, -2147483648
  fmov s0, w0
  ret

Совсем не так оптимально, как Clang, но в целом ОК.

Итак. Код, приведённый в качестве якобы "неправильного" примера, если и будет где-то работать неправильно, то для этого нужно очень сильно постараться.
Но для того, чтобы быть уверенными, следует использовать ключевые слова __restrict и __noalias.

Продолжение следует.
Tags: C и C++, arm, arm7, llvm, программирование
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 3 comments