`movaps` против` movups` в G CC: как это решается? - PullRequest
2 голосов
/ 13 апреля 2020

Я недавно исследовал segfault в программном обеспечении, скомпилированном с G CC 8. Код выглядел следующим образом (это всего лишь набросок)

struct Point
{
  int64_t x, y;
};

struct Edge
{
  // some other fields
  // ...
  Point p; // <- at offset `0xC0`

  Edge(const Point &p) p(p) {}
};

Edge *create_edge(const Point &p)
{
  void *raw_memory = my_custom_allocator(sizeof(Edge));
  return new (raw_memory) Edge(p);
}

Ключевым моментом здесь является то, что my_custom_allocator() возвращает указатели на невыровненную память. Сбой кода, потому что для копирования исходной точки p в поле Edge::p нового объекта компилятор использовал пару movdqu / movaps в [встроенном] коде конструктора

movdqu 0x0(%rbp), %xmm1  ; read the original object at `rbp`
...
movaps %xmm1, 0xc0(%rbx) ; store it into the new `Edge` object at `rbx` - crash!

Сначала кажется, что здесь все ясно: память выровнена неправильно, movaps вылетает. Моя вина.

Но так ли это?

Пытаясь воспроизвести проблему с Годболтом, я замечаю, что G CC 8 на самом деле пытается справиться с этим довольно разумно. Когда он уверен, что память правильно выровнена, он использует movaps, как в моем коде. Это

#include <new>
#include <cstdlib>

struct P { unsigned long long x, y; };

unsigned char buffer[sizeof(P) * 100];

void *alloc()
{
  return buffer;
}

void foo(const P& s)
{
  void *raw = alloc();
  new (raw) P(s);
}

приводит к этому

foo(P const&):
    movdqu  xmm0, XMMWORD PTR [rsi]
    movaps  XMMWORD PTR buffer[rip], xmm0
    ret

https://godbolt.org/z/a3uSid

Но когда он не уверен, он использует movups. Например, если я «скрою» определение распределителя в приведенном выше примере, он выберет movups в том же коде

foo(P const&):
    push    rbx
    mov     rbx, rdi
    call    alloc()
    movdqu  xmm0, XMMWORD PTR [rbx]
    movups  XMMWORD PTR [rax], xmm0
    pop     rbx
    ret

https://godbolt.org/z/cNKe5A

Итак, если он должен вести себя так, почему он использует movaps в программном обеспечении, о котором я упоминал в начале этого поста? В моем случае реализация my_custom_allocator() не видна компилятору в точке вызова, поэтому я ожидаю, что G CC выберет movups.

Какие еще факторы могут быть здесь задействованы? Это ошибка в G CC? Как я могу заставить G CC использовать movups, желательно везде?

Ответы [ 3 ]

3 голосов
/ 14 апреля 2020

Обновление: alignof(Edge) было 16 из-за long double в x86-64 System V, так что это UB, чтобы иметь один по менее выровненному адресу. Это говорит G CC, что безопасно использовать movaps.

IDK, почему при загрузке из (%rbp) также не используется movaps. Я думал, что подразумеваемый Edge не будет выровнен по 16 байтам, поэтому есть целый раздел этого ответа, основанный на этом предположении (которое я переместил в конец).


Для некоторых типов может потребоваться 16- выравнивание байтов, в частности long double

alignof(max_align_t) == 16 в системе x86-64 V. Для замены malloc требуется возврат памяти, по крайней мере, такой, для выравнивания, для выделений 16 байтов или больше.

(Меньшие выделения, конечно, не могут содержать 16-байтовый объект и, следовательно, не требуют 16-байтового выравнивания. Вы можете запросить конкретный c экземпляр объекта для быть выровненным по alignas(16) int foo;, но если сам тип имеет более высокое выравнивание, он также имеет большее sizeof, поэтому массив все равно будет подчиняться нормальным правилам, а также чтобы каждый элемент удовлетворял требованию выравнивания.)

См. Также Почему при выравнивании доступа к памяти mmap иногда возникает ошибка на AMD64? , когда автоматическая векторизация со смещением uint16_t* приводит к ошибке. Также Pascal Блог Cuoq о выравнивании и наличии объектов с меньшим выравниванием, чем alignof(T), является неопределенным поведением, и то, как допущение отсутствия UB глубоко подходит для компиляторов.


Выбор инструкций

G CC и clang используют movaps всякий раз, когда они могут доказать, что память должна быть достаточно выровнена. (При условии отсутствия UB). На Core2 и более ранних версиях, а также K10 и более ранних версиях невыровненные хранилища работают медленно, даже если во время выполнения происходит выравнивание памяти.

Nehalem и Bulldozer изменили это, но G CC все еще использует movaps даже при -mtune=haswell или даже vmovaps с -march=haswell, хотя это может выполняться только на процессорах с дешевыми vmovups.

MSV C, а я CC никогда не использую movaps, что негативно сказывается на производительности на очень старых процессорах, но иногда позволяя избежать смещения данных. Они будут складывать выровненные нагрузки в операнды памяти для инструкций SSE, таких как paddd xmm0, [rdi] (что требует выравнивания, в отличие от эквивалента AVX1), поэтому они все равно будут создавать код, который дает сбой при смещении иногда , но обычно только с включенной оптимизацией. ИМО это не особенно здорово.


alignof(Point) должно быть только 8 (наследуя выравнивание своего наиболее выровненного члена, int64_t). Таким образом, G CC может доказать только 8-байтовое выравнивание для произвольного Point, а не 16.

Для хранения данных c, G CC может знать, что он решил выровнять массив на 16 и, следовательно, может использовать movaps / movdqa для загрузки с него. (Кроме того, x86-64 System V ABI требует, чтобы массивы stati c размером 16 байт или более были выровнены на 16, поэтому G CC может принять это даже для глобала extern unsigned char buffer[], определенного в некотором другом модуле компиляции.)

Вы не показали определение для Edge, поэтому IDK, почему он имеет 16-байтовое выравнивание, но, возможно, alignof(Edge) == 16? В противном случае да, это может быть ошибка компилятора.

Но тот факт, что он загружает исходный объект Edge из стека с movups, может показаться, что alignof(Edge) < 16


Возможно raw_memory = __builtin_assume_aligned(raw_memory, 8); может помочь? IDK, если это может сказать G CC принять более низкое выравнивание, чем он уже предполагал, исходя из других факторов.


Вы могли бы сказать G CC, что Edge (или int в этом отношении) всегда можно выровнять, определив typedef следующим образом:

typedef long __attribute__((aligned(1), may_alias)) unaligned_aliasing_long;

may_alias на самом деле ортогонально выравниванию, но стоит упомянуть, потому что один из вариантов использования случаи для этого будут загружены из буфера char[] для анализа потока байтов. В этом случае вы хотели бы оба. Это альтернатива использованию memcpy(tmp, src, sizeof(tmp)); для выполнения невыровненных безопасных строго псевдонимов нагрузок.

G CC использует may_alias для определения __m128 и may_alias,aligned(1) как часть определения _mm_loadu_ps (intrinsi c для не выровненных SIMD-нагрузок, таких как movups). (Вам не нужно may_alias для загрузки вектора с плавающей точкой из массива float, но вам нужно may_alias для загрузки его из чего-то другого.) См. Также Is `reinterpret_cast`ing между аппаратным указателем вектора SIMD и соответствующим типом неопределенное поведение?

И посмотрите Почему strlen glibc должен быть настолько сложным для быстрой работы? для скалярного кода, который я считаю безопасен для заниженных / псевдонимов unsigned long, в отличие от альтернативной реализации glib c C. (Который должен быть скомпилирован без -flto, чтобы он не мог встраиваться в другие функции glib c и прерываться из-за нарушения строгого псевдонима.)


Распределители и предполагаемое выравнивание

(Этот раздел был написан, предполагая, что alignof(Edge) < 16. Здесь дело не в этом, и атрибуты функции могут быть полезны для понимания, даже если они не являются причиной проблемы. И, вероятно, также не являются приемлемым обходным путем. )

Возможно, вы сможете использовать __attribute__ ((assume_aligned (8))) на вашем распределителе, чтобы сообщить G CC о выравнивании возвращаемого указателя.

G CC, возможно, для некоторых допускается причина в том, что ваш распределитель возвращает память, используемую для любого объекта (и alignof(max_align_t) == 16 на x86-64 System V из-за long double и других вещей, а также на Windows x64).

Если это не случай, вы можете сказать это. Это mmap неправильное выравнивание Q & A , мы можем видеть, что G CC «знает» о malloc и относится к нему специально. Но если ваша функция не имеет имени, определенного в ISO C или C ++, или атрибутов GNU C, это было бы удивительно. IDK, это лучшее предположение на основе того, что вы показали, если это не ошибка компилятора. (Это возможно.)

С руководство G CC :

void* my_alloc1 (size_t) __attribute__((assume_aligned (16)));
void* my_alloc2 (size_t) __attribute__((assume_aligned (32, 8)));

объявляет, что my_alloc1 возвращает 16-байтовые выровненные указатели и что my_alloc2 возвращает указатель, значение которого по модулю 32 равно 8.

Я не знаю, почему предполагается, что void*, возвращенный функцией и приведенный к другому типу, будет иметь хоть и больше выравнивания, чем тип создаваемого объекта. Мы можем использовать movups для загрузки Edge откуда-то. Это может указывать на то, что alignof(Edge) < 16.

Также имеет значение __attribute__((alloc_size(1))), чтобы сказать G CC, что первый аргумент функции - это размер. Если ваша функция принимает явное выравнивание в качестве аргумента, используйте alloc_align (position), чтобы указать это, в противном случае - нет.

1 голос
/ 14 апреля 2020

Как правильно заявили другие участники в уже опубликованных ответах, запускающим фактором являются требования к выравниванию моего типа данных. Специфическим c виновником оказалось поле данных long double, также присутствующее в моем struct, которое первоначально привлекло мое внимание. Это long double поле данных заставило требование выравнивания всей структуры стать 16.

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

Но практически (ссылаясь на специфичное для реализации c поведение G CC), это не выглядит так ясно. Здесь все еще есть странная особенность в поведении G CC.

Выше, в моем исходном вопросе вы можете увидеть пример структуры с требованием выравнивания 8 (предположим, что у него нет long double поля в нем). С этим типом данных G CC ведет себя так, как я уже описал выше:

  1. Когда выравнивание raw_pointer очевидно для компилятора и известно, что оно равно 16 или больше, G CC генерирует movaps инструкции.
  2. Когда выравнивание raw_pointer очевидно для компилятора и известно, что оно меньше 16, G CC генерирует movups инструкции.
  3. Когда выравнивание raw_pointer неочевидно для компилятора, оно генерирует инструкции movups.

Таким образом, в этом случае G CC безопасен, он ведет себя разрешительно / оборонительно. Даже если данные не выровнены, на практике код будет работать «как положено». (Может быть, я что-то упускаю, и можно сделать это GPF с 8-ю выравниванными данными, но, чего бы это ни стоило, я еще не сталкивался с этим.)

Но как только мы перейдем к 16 Выровненная структура (скажем, путем добавления поля long double), G CC logi c изменяется на следующее:

  1. Когда выравнивание raw_pointer очевидно для компилятора и известно, что оно составляет 16 или больше, G CC генерирует movaps инструкции.
  2. Когда выравнивание raw_pointer очевидно для компилятора и известно, что оно меньше 16, G CC генерирует movups инструкции.
  3. Когда выравнивание raw_pointer не очевидно для компилятора, оно генерирует movaps инструкции (да, movaps!)

Обратите внимание на третий момент: эта маленькая деталь является причиной создания GPF в вышеупомянутом проекте. Вот небольшой пример того же кратера sh: http://coliru.stacked-crooked.com/a/c5cd2be91ebba41e. (Кстати, Clang, кажется, еще более строг в этом отношении. 16 данных выровнены? Используйте movaps, даже если указатель «явно» не выровнен.)

Глядя на ситуации 1 и 2, кажется, что с данными с 16-ю выравниванием G CC также как бы намеревался вести себя разрешительно / оборонительно, как и с данными с 8-ю выравниванием. Но по какой-то причине для ситуации 3 она выбирает go с movaps вместо movups. Почему несоответствие с 8-уровневым процессом принятия решений?

Опять же, очевидно, что "поведение не определено, это ваша вина". Но вышеприведенное несоответствие между решениями, принятыми для данных с 8-ю и 16-ю данными, кажется мне немного странным. Если это сделано преднамеренно, по крайней мере было бы полезно иметь опцию, чтобы G CC обрабатывал 16-выровненные данные таким же образом, как он обрабатывает 8-выровненные данные, т. Е. Используйте movups, когда все не совсем прозрачно.

Со второй мысли, здесь действительно нет "несоответствия". Лог c равен solid: с данными с 8 выравниванием G CC не может принять универсальную применимость movaps, поэтому он имеет для защиты, даже если данные идеально выровнены по 8. С 16-ю выровненными данными G CC может формально вывести применимость movaps во всех случаях, поэтому не имеет для защиты.


В качестве быстрого обходного пути для те, кто по какой-то причине не могут или не хотят 16-ти выравнивать свои структуры (экономия памяти, устаревшие проекты и т. д. c.): объявление полей long double как packed "убивает" их требование выравнивания. Если при этом вы успешно уменьшите требование выравнивания структуры до 8 или менее, вернется старое доброе разрешающее поведение G CC.

1 голос
/ 14 апреля 2020

Поскольку структура Edge имеет требование выравнивания, определяемое компилятором, компилятор может предположить, что все объекты этого типа правильно выровнены. Если ваш пользовательский распределитель не возвращает указатель на правильно выровненную память, использование вами объекта по этому адресу приводит к неопределенному поведению.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...