Почему этот класс-обёртка C ++ не был выделен? - PullRequest
0 голосов
/ 07 января 2019

РЕДАКТИРОВАТЬ - что-то не так с моей системой сборки. Я до сих пор точно выясняю, что именно, но gcc давал странные результаты (даже если это файл .cpp), но как только я использовал g++, он работал как ожидалось.


Это очень сокращенный тестовый пример для чего-то, с чем у меня были проблемы, когда использование числового класса-обёртки (который, как я думал, будет выделен) сделал мою программу в 10 раз медленнее.

Это не зависит от уровня оптимизации (пробовал с -O0 и -O3).

Мне не хватает какой-то детали в моем классе-обёртке?


C ++

У меня есть следующая программа, в которой я определяю класс, который переносит double и предоставляет оператор +:

#include <cstdio>
#include <cstdlib>

#define INLINE __attribute__((always_inline)) inline

struct alignas(8) WrappedDouble {
    double value;

    INLINE friend const WrappedDouble operator+(const WrappedDouble& left, const WrappedDouble& right) {
        return {left.value + right.value};
    };
};

#define doubleType WrappedDouble // either "double" or "WrappedDouble"

int main() {
    int N = 100000000;
    doubleType* arr = (doubleType*)malloc(sizeof(doubleType)*N);
    for (int i = 1; i < N; i++) {
        arr[i] = arr[i - 1] + arr[i];
    }

    free(arr);
    printf("done\n");

    return 0;
}

Я думал, что это скомпилирует одно и то же - он выполняет те же вычисления, и все встроено.

Однако это не так - он дает больший и более медленный результат, независимо от уровня оптимизации.

(Этот конкретный результат не значительно медленнее, но мой фактический вариант использования включает в себя больше арифметики.)

РЕДАКТИРОВАТЬ - Я знаю, что это не создание элементов моего массива. Я думал, что это может привести к меньшему количеству ASM, поэтому я мог бы лучше понять его, но я могу изменить его, если это проблема.

РЕДАКТИРОВАТЬ - Я также знаю, что я должен использовать new[] / delete[]. К сожалению, gcc отказался компилировать это, хотя это было в файле .cpp. Это был признак того, что моя система сборки была испорчена, что, вероятно, является моей настоящей проблемой.

РЕДАКТИРОВАТЬ - Если я использую g++ вместо gcc, он выдаст идентичный вывод.


РЕДАКТИРОВАТЬ - Я разместил неверную версию ASM (-O0 вместо -O3), поэтому этот раздел бесполезен.

Монтаж

Я использую gcc XCode на моем Mac в 64-битной системе. Результат тот же, кроме тела цикла for.

Вот что это выдает для тела цикла, если doubleType равно double:

movq    -16(%rbp), %rax
movl    -20(%rbp), %ecx
subl    $1, %ecx
movslq  %ecx, %rdx
movsd   (%rax,%rdx,8), %xmm0    ## xmm0 = mem[0],zero
movq    -16(%rbp), %rax
movslq  -20(%rbp), %rdx
addsd   (%rax,%rdx,8), %xmm0
movq    -16(%rbp), %rax
movslq  -20(%rbp), %rdx
movsd   %xmm0, (%rax,%rdx,8)

Версия WrappedDouble намного длиннее:

movq    -40(%rbp), %rax
movl    -44(%rbp), %ecx
subl    $1, %ecx
movslq  %ecx, %rdx
shlq    $3, %rdx
addq    %rdx, %rax
movq    -40(%rbp), %rdx
movslq  -44(%rbp), %rsi
shlq    $3, %rsi
addq    %rsi, %rdx
movq    %rax, -16(%rbp)
movq    %rdx, -24(%rbp)
movq    -16(%rbp), %rax
movsd   (%rax), %xmm0           ## xmm0 = mem[0],zero
movq    -24(%rbp), %rax
addsd   (%rax), %xmm0
movsd   %xmm0, -8(%rbp)
movsd   -8(%rbp), %xmm0         ## xmm0 = mem[0],zero
movsd   %xmm0, -56(%rbp)
movq    -40(%rbp), %rax
movslq  -44(%rbp), %rdx
movq    -56(%rbp), %rsi
movq    %rsi, (%rax,%rdx,8)

Ответы [ 3 ]

0 голосов
/ 07 января 2019

Это встроено , но не оптимизировано, потому что вы скомпилировали с -O0 (по умолчанию) . Это генерирует asm для согласованной отладки, позволяя изменить любую переменную C ++, находясь в точке останова на любой строке.

Это означает, что компилятор выдает все из регистров после каждого оператора и перезагружает то, что ему нужно для следующего. Поэтому больше операторов для выражения одной и той же логики = более медленный код, независимо от того, находятся ли они в одной функции или нет. Почему clang производит неэффективный asm для этой простой суммы с плавающей запятой (с -O0)? объясняет более подробно.

Обычно -O0 не встроенные функции, но он уважает __attribute__((always_inline)).

Справка по оптимизации цикла C для окончательного назначения объясняет, почему бенчмаркинг или настройка с помощью -O0 абсолютно бессмысленны. Обе версии являются нелепым мусором для производительности.


Если бы оно не было встроенным, в цикле была бы инструкция call.

Asm фактически создает указатели в регистрах для const WrappedDouble& left и right. (очень неэффективно, используя несколько инструкций вместо одной lea. addq %rdx, %rax - последний шаг в одной из них.)

Затем эти аргументы-указатели перенаправляются в стековую память, поскольку они являются реальными переменными и должны находиться в памяти, где отладчик может их модифицировать. Вот что делают movq %rax, -16(%rbp) и %rdx ...

После перезагрузки и разыменования этих указателей результат addsd (добавление скалярного двойного) сам возвращается в локальную память стека с помощью movsd %xmm0, -8(%rbp). Это не именованная переменная, это возвращаемое значение функции.

Затем он перезагружается и снова копируется в другое место в стеке, затем, наконец, arr и i загружаются из стека вместе с результатом double operator+, который сохраняется в arr[i] с помощью movq %rsi, (%rax,%rdx,8). (Да, LLVM использовал 64-разрядное целое число mov, чтобы скопировать double в то время. Ранее использовался SSE2 movsd.)

Все эти копии возвращаемого значения находятся на критическом пути для цепочки зависимостей, переносимых циклом, потому что следующая итерация читает arr[i-1]. Эти ~ 5 или 6 циклических задержек пересылки хранилища действительно сложение против 3 или 4 цикла FP add задержка.


Очевидно, что массово неэффективно. С включенной оптимизацией, gcc и clang без проблем вставляют и оптимизируют вашу оболочку.

Они также оптимизируют, сохраняя результат arr[i] в регистре для использования в качестве результата arr[i-1] на следующей итерации. Это позволяет избежать задержки передачи ~ 6 циклов, которая в противном случае была бы внутри цикла, если бы она делала asm как источник.

т.е. оптимизированный ассм выглядит примерно так: C ++:

double tmp = arr[0];   // kept in XMM0

for(...) {
   tmp += arr[i];   // no re-read of mmeory
   arr[i] = tmp;
}

Забавно, но clang не потрудился инициализировать tmp (xmm0) перед циклом, потому что вы не удосужились инициализировать массив . Странно это не предупреждает о UB. На практике большая malloc с реализацией glibc даст вам свежие страницы из ОС, и все они будут содержать нули, то есть 0.0. Но Clang даст вам все, что осталось в XMM0! Если вы добавите ((double*)arr)[0] = 1;, clang загрузит первый элемент перед циклом.

К сожалению, компилятор не знает, как сделать что-то лучше, чем для расчета суммы префикса. Посмотрите параллельную префиксную (накопительную) сумму с SSE и SIMD префиксную сумму на процессоре Intel , чтобы узнать, как ускорить этот процесс еще на 2 раза и / или распараллелить его.

Я предпочитаю синтаксис Intel, но Проводник компилятора Godbolt может предоставить вам синтаксис AT & T, как в вашем вопросе, если хотите.

# gcc8.2 -O3 -march=haswell -Wall
.LC1:
    .string "done"
main:
    sub     rsp, 8
    mov     edi, 800000000
    call    malloc                  # return value in RAX

    vmovsd  xmm0, QWORD PTR [rax]   # load first elmeent
    lea     rdx, [rax+8]            # p = &arr[1]
    lea     rcx, [rax+800000000]    # endp = arr + len

.L2:                                   # do {
    vaddsd  xmm0, xmm0, QWORD PTR [rdx]   # tmp += *p
    add     rdx, 8                        # p++
    vmovsd  QWORD PTR [rdx-8], xmm0       # p[-1] = tmp
    cmp     rdx, rcx
    jne     .L2                        # }while(p != endp);

    mov     rdi, rax
    call    free
    mov     edi, OFFSET FLAT:.LC0
    call    puts
    xor     eax, eax
    add     rsp, 8
    ret

Clang немного разворачивается, и, как я уже сказал, не удосуживается инициировать его tmp.

# just the inner loop from clang -O3
# with -march=haswell it unrolls a lot more, so I left that out.
# hence the 2-operand SSE2 addsd instead of 3-operand AVX vaddsd
.LBB0_1:                                # do {
    addsd   xmm0, qword ptr [rax + 8*rcx - 16]
    movsd   qword ptr [rax + 8*rcx - 16], xmm0
    addsd   xmm0, qword ptr [rax + 8*rcx - 8]
    movsd   qword ptr [rax + 8*rcx - 8], xmm0
    addsd   xmm0, qword ptr [rax + 8*rcx]
    movsd   qword ptr [rax + 8*rcx], xmm0
    add     rcx, 3                            # i += 3
    cmp     rcx, 100000002
    jne     .LBB0_1                      } while(i!=100000002)

Apple XCode gcc на самом деле является замаскированным LLVM в современных системах OS X.

0 голосов
/ 09 января 2019

Для дальнейшего использования (мой и любой другой): я видел несколько разных вещей:

  1. Проект XCode, который я использовал изначально (который я адаптировал, но не создал), каким-то образом настроен так, что даже в сборке Release не использовалось -O3.

  2. Использование gcc для кода C ++ - плохая идея. Даже при компиляции файла .cpp он по умолчанию не ссылается на стандартную библиотеку. Использование g++ намного плавнее.

  3. Самое интересное (для меня): даже когда обертка правильно вставлялась, обертка нарушала некоторые оптимизации !

Третьим моментом было то, что вызвало замедление в моем исходном коде (не указанном здесь), которое привело меня по этому пути.

Когда вы добавляете кучу значений с плавающей точкой, например a + b + c + d, нельзя переупорядочивать c или d, потому что (поскольку значения с плавающей запятой являются приблизительными), это может привести к слегка другому результату. Однако разрешено поменять местами a и b, потому что это первое добавление симметрично - и в моем случае это позволяет использовать инструкции SIMD для 64-битных сборок.

Однако, когда использовалась обертка, она не передавала информацию о том, что первый + фактически является коммутативным! Он покорно все обговорил, но каким-то образом не осознавал, что ему по-прежнему разрешено менять первые два аргумента. Когда я переупорядочил суммы вручную соответствующим образом, мои две версии получили одинаковую производительность.

0 голосов
/ 07 января 2019

Обе версии приводят к одинаковому ассемблерному коду с g++ и clang++ при включении оптимизаций с -O3.

...