Флаг для генерации «детерминированных» операций с плавающей запятой относительно.выравнивание указателя с помощью 'fast-math'? - PullRequest
4 голосов
/ 09 июля 2019

Опция -ffast-math в gcc позволяет компилятору переупорядочивать операции с плавающей запятой для более быстрого выполнения.

Это может привести к небольшим различиям между результатами этих операций в зависимости от выравнивания указателей.Например, в x64 некоторые оптимизированные инструкции (AVX) выполняются быстрее для указателей, которые выровнены по 128 битам, поэтому имеет смысл, что компилятору может быть разрешено это делать.

Вот пример простой программы, которая показывает такое поведение на современном процессоре:

#include <stdio.h>
#include <stdint.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>


double sum(int size, double *a) {
        double r = 0.0;
        for (int k = 0;k < size; k ++) {
          r+= a[k];
        }
        return r;
}

void init(int size, double *a) {
  srand(0);
  for (int k = 0; k < size; k ++) {
          a[k] = ((double) rand()) / RAND_MAX;
  }
}


void test(int size, double *ref, double *arr) {
  init (size, ref);
  memcpy (arr, ref, sizeof *arr * size);

  printf("Alignment: ref:%d  arr:%d diff:%g\n",
    (int)((uintptr_t)ref % 32),
    (int)((uintptr_t)arr % 32),
    fabs(sum(size, arr) - sum(size, ref)));
}


int main(int argc, char **argv) {
  int size = argc <= 1 ? 100 : atoi(argv[1]); // (don't do that at home)
  double *array1 = malloc(size * sizeof *array1);
  double *array2 = malloc((size + 4) * sizeof *array2);
  printf("size = %d\n", size);
  if (array1 && array2) {
    for (int k = 0;k < 4;k ++){
      test(size, array1, array2 + k);
    }
  }
}

, которая может выводиться при компиляции с -Ofast:

$ ./test 100000
size = 100000
Alignment: ref:16  arr:16 diff:0
Alignment: ref:16  arr:24 diff:7.27596e-12
Alignment: ref:16  arr:0 diff:0
Alignment: ref:16  arr:8 diff:7.27596e-12

Вопрос

Существует ли магический флаг, который вежливо просит компилятор не генерировать код, чувствительный к выравниванию указателей, без полного запрета «быстрой математической» оптимизации?

Подробнее

Вот конфигурация моего gcc:

Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.4.0-1ubuntu1~18.04.1' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)

Я просто скомпилировал приведенный выше код, используя всего лишь «gcc -Ofast».

Глядя на сгенерированный ассемблерный код, кажется, что компилятор реализует следующий псевдокод:

typedef struct {
    double lower;
    double upper; 
} packed_double128; 

double sum6(int size, double *a) { ... an array of size smaller than 6 and at least one in an "unrolled" fashion ... }

double sum(int size, double *a) {
    if (size == 0) {
        return 0.0;
    } else if (size - 1 <= 5) {
        int delta;
        double tmp; 
        if (((intptr_t) a) % 16) {
           delta = 1; 
           tmp = a[0];
        } else {
            delta = 0; 
            tmp = 0.0;
        }        
        packed_double128 res = {0., 0.}; 
        double *p = a + delta; 
        assert (((intptr_t) p) % 16 == 0); // p is aligned !
        for (k = 0; k < (size - delta) / 2; k ++) {
            res.lower += *p++;
            res.upper += *p++;            
        }
        double res = res.lower + res.upper + tmp; 
        int remain = size - 2*((size - delta) / 2); 
        if (remain) 
          return res + sum6(remain, p); 
        else 
          return res; 
    } else {
        return sum6(size, a); 
    }
}

Например, если a является массивом {0, 1, 2, .., 10} sum вычисляет:

  • ((0 + 2 + 4 + 6 + 8) + (1 + 3 + 5 + 7 + 9)) + 10если он "выровнен"

  • ((2 + 4 + 6 + 8 + 10) + (1 + 3 + 5 + 7 + 9)) + 1, если не"выровненный"

Вот код сборки (комментарий мой и может быть неправильным):

sum: // double sum (int size, double *a)
.LFB61:
    .cfi_startproc
    testl   %edi, %edi       // size = 0 ?
    jle .L8 ;
    movq    %rsi, %rax  ;    // rax = a
    leal    -1(%rdi), %edx ; // edx = size - 1
    shrq    $3, %rax         // rax = rax >> 3
    andl    $1, %eax         // rax = rax % 2
    // now eax contains either 1 or 0 depending the alignment, let's call this value "delta"
    cmpl    $5, %edx         // size - 1 <= 5 ?
    jbe .L9
    testl   %eax, %eax       // a % 16 = 0 ? if so, skip the next two mov (and initialize xmm2 to 0)
    je  .L10
    movsd   (%rsi), %xmm2    // xmm2 = a[0] (we put aside the first element)
    movl    $1, %ecx         // ecx = 1
.L4:
    movl    %edi, %r9d          // r9d = size
    pxor    %xmm1, %xmm1        // xmmm1 = 0
    subl    %eax, %r9d          // r9d = size - delta
    leaq    (%rsi,%rax,8), %rdx // rdx = a + eax * 8 (which is "&a[rax + delta]")
    xorl    %eax, %eax          // eax = 0
    movl    %r9d, %r8d
    shrl    %r8d                // r8d = (size - delta) / 2
    .p2align 4,,10
    .p2align 3
.L5: // HERE IS THE "real loop"
    addl    $1, %eax            // eax ++
    addpd   (%rdx), %xmm1       // xmm1 += *rdx (thas's adding 2 doubles into xmm1)
    addq    $16, %rdx           // rdx += 2 * sizeof(double)
    cmpl    %r8d, %eax          // eax < (size - delta) / 2 ?
    jb  .L5
    // Here xmm1 contains two halves of our sum:
    // one double is the sum of odds elements and the other the sums of even elements
    movdqa  %xmm1, %xmm0        // xmm0 = xmm1
    movl    %r9d, %edx          // edx = size - delta
    andl    $-2, %edx           // edx = r9d - (r9d % 2)
    psrldq  $8, %xmm0           // xmm0[0] = xmm0[1]; xmm0[1] = 0.0;
    addpd   %xmm0, %xmm1        // xmm1 += xmm0 (which now means that xmm1[0] contains the final result \o/ )
    cmpl    %edx, %r9d          // r9d = r9d - (r9d % 2) ? eg. r9d % 2 = 0 ?
    leal    (%rdx,%rcx), %eax   // eax = rdx + 1
    movapd  %xmm1, %xmm0
    addsd   %xmm2, %xmm0        // xmm0 = xmm1 + xmm2[0] Here we add the skipped element in case of misalignment
    je  .L13
.L3: // BORING UNROLLED LOOP FOR SMALL ARRAYS (size <= 6)
    movslq  %eax, %rdx
    addsd   (%rsi,%rdx,8), %xmm0
    leal    1(%rax), %edx
    cmpl    %edx, %edi
    jle .L1
    movslq  %edx, %rdx
    addsd   (%rsi,%rdx,8), %xmm0
    test    2(%rax), %edx
    cmpl    %edx, %edi
    jle .L1
    movslq  %edx, %rdx
    addsd   (%rsi,%rdx,8), %xmm0
    leal    3(%rax), %edx
    cmpl    %edx, %edi
    jle .L1
    movslq  %edx, %rdx
    addsd   (%rsi,%rdx,8), %xmm0
    leal    4(%rax), %edx
    cmpl    %edx, %edi
    jle .L1
    addl    $5, %eax
    movslq  %edx, %rdx
    cmpl    %eax, %edi
    addsd   (%rsi,%rdx,8), %xmm0
    jle .L1
    cltq
    addsd   (%rsi,%rax,8), %xmm0
    ret
    .p2align 4,,10
    .p2align 3
.L8:
    pxor    %xmm0, %xmm0
.L1:
    rep ret
    .p2align 4,,10
    .p2align 3
.L10:
    xorl    %ecx, %ecx
    pxor    %xmm2, %xmm2
    jmp .L4
    .p2align 4,,10
    .p2align 3
.L13:
    rep ret
    .p2align 4,,10
    .p2align 3
.L9:
    xorl    %eax, %eax
    pxor    %xmm0, %xmm0
    jmp .L3
    .cfi_endproc
...