Использование C union со встроенными SSE в Cython приводит к SIGSEGV - PullRequest
0 голосов
/ 05 июля 2018

У меня есть следующий фрагмент кода C, который я хочу обернуть с помощью Cython и использовать в моей программе Python:

typedef union my_mat4
{
    float Elements[4][4];
#ifdef MAT4_MATH__USE_SSE
    __m128 Rows[4];
#endif
} my_mat4;

static inline my_mat4 init_my_mat4(void)
{
    my_mat4 Result = {0};
    return (Result);
}

Я заключаю этот код в mat4.pxd следующим образом:

cdef extern from "mat4.h":
    ctypedef union my_mat4:
        float Elements[4][4]

    my_mat4 init_my_mat4()

cdef class MyClass:
    cdef my_mat4 m
    cdef object o

и код mat4.pyx:

cdef class MyClass:
    def __cinit__(self):
        self.m = init_my_mat4()

Когда я использую MyClass в Python

my_class = MyClass()

Python завершается с сообщением Процесс завершен с кодом выхода 139 (прерван сигналом 11: SIGSEGV) .

Однако, когда я отключаю MAT4_MATH__USE_SSE из объединения C, программа работает нормально. Даже когда я удаляю cdef object o из MyClass, все работает нормально.

Я много искал решение, но ничего не смог найти. Это какая-то проблема с выравниванием?

Спасибо, A.

РЕДАКТИРОВАТЬ 1:

Программа Python аварийно завершает работу только тогда, когда импортируется модуль десятичный и __m128 компилируется в модуле расширения Cython, поэтому:

import decimal
my_class = MyClass()

аварийно завершает работу интерпретатора (на панели запуска программы Ubuntu появляется значок с изображением заставки)

РЕДАКТИРОВАТЬ 2:

Как сказал @DavidW в комментариях, десятичная дробь импорта в этом случае, вероятно, не имеет значения - для него программа segfaults без импорта запускается с импортом.

РЕДАКТИРОВАТЬ 3:

Как писал @PeterCordes, почти наверняка это проблема смещения. Решение состоит в том, чтобы удалить элемент __m128 из объединения и создать предложенный макрос load_vec() или аналогичный с _mm_loadu_ps.

.

РЕДАКТИРОВАТЬ 4:

Другое решение использует unaligned__m128, определенный как typedef float __attribute((vector_size(16), aligned(4))) unaligned__m128; вместо __m128 в объединении. Спасибо @ PeterCordes.

1 Ответ

0 голосов
/ 05 июля 2018

Это какая-то проблема с выравниванием?

Почти наверняка.

Компиляторы

C предполагают, что объект __m128 имеет 16-байтовое выравнивание, и используют movaps, чтобы загрузить / сохранить его, или использовать его как операнд памяти для других инструкций SSE (например, * 1011). *). Эти применения будут давать сбой во время выполнения, если указатель не имеет 16-байтовое выравнивание.

Но вы не сказали Python выделить float Elements[4][4] с какой-либо гарантией выравнивания, поэтому передача указателей на C даст вам недействительные union объекты, которые нарушают требование выравнивания объединения достаточно для наиболее выровненного члена.


Если вы не можете заставить Python гарантировать 16-байтовое выравнивание ваших объектов , то вам придется изменить C, чтобы он все еще работал (немного менее эффективно). Компиляция с включенным AVX (gcc -O3 -march=native на процессорах AVX) позволит компилятору использовать невыровненные 16-байтовые векторы в качестве операндов памяти. Но он по-прежнему не сделает смещенным __m128 сейфом, потому что он все равно будет храниться с vmovaps, а не vmovups.

Современное оборудование имеет эффективную поддержку выравнивания нагрузки, но разбиение строк кэша все еще не идеально. Число команд также хуже, потому что в AVX компилятору придется использовать отдельные movups загрузки вместо addps xmm0, [mem] для данных, которые нужно загружать только один раз.

В C удалите элемент __m128 и используйте _mm_loadu_ps() для выравнивания нагрузки.

typedef struct my_mat4 { float Elements[4][4]; } my_mat4;

static inline
__m128 load_vec(const struct my_mat4 *m4, size_t idx) {
    _mm_loadu_ps(&m4->Elements[idx][0]);
}

С GNU C: переопределите ваш союз с версией без выравнивания __m128

Было бы наиболее эффективно заставить Python выравнивать ваши объекты, но если нет, то это позволит вам скомпилировать существующий код с одним изменением вашего объекта:

__m128 определяется в терминах собственных векторов GNU C в xmmintrin.h # 69 . (Другие компиляторы, которые поддерживают расширения GNU, совместимы, по крайней мере, Clang.)

typedef float __m128 attribute ((vector_size (16), may_alias));

Заголовок уже определяет невыровненный __m128_u, который также использует aligned(1). Мы можем использовать aligned(4), чтобы гарантировать, что он как минимум выровнен по границе float, если это поможет.

Это просто работает, потому что разные версии выравнивания одного и того же векторного типа свободно конвертируемы, поэтому код, передающий его встроенным функциям, компилируется без предупреждений (даже при -Wall).

typedef float __attribute((vector_size(16), aligned(4))) unaligned__m128;
// I left out may_alias, only matters if you're using unaligned__m128* to load from non-float data.
// Probably doesn't hurt code-gen if you aren't using unaligned__m128* at all, just objects

   //#define __m128 unaligned__m128   // not needed

typedef union my_mat4 {
     float Elements[4][4];
     unaligned__m128 Rows[4];
} my_mat4;

Функции, использующие этот тип, прекрасно компилируются ( gcc8.1 в проводнике компилятора Godbolt ). (Вы могли бы также написать m4->Rows[1] + m4->Rows[2], даже в C, а не в C ++, потому что собственные векторы GNU C отображают операторы C на операции для каждого элемента.

__m128 use_row(union my_mat4 *m4) {
    __m128 tmp = _mm_add_ps(m4->Rows[1], m4->Rows[2]);
    m4->Rows[3] = tmp;
    return tmp;
}

Всего с -O3 (без марша) мы получаем

    movups  xmm0, XMMWORD PTR [rdi+32]    # unaligned loads
    movups  xmm1, XMMWORD PTR [rdi+16]
    addps   xmm0, xmm1
    movups  XMMWORD PTR [rdi+48], xmm0    # unaligned store
    ret

Но с -mavx (например, с помощью -march=haswell) мы получаем

use_row(my_mat4*):
    vmovups xmm1, XMMWORD PTR [rdi+32]
    vaddps  xmm0, xmm1, XMMWORD PTR [rdi+16]    # unaligned memory source is ok for AVX
    vmovups XMMWORD PTR [rdi+48], xmm0
    ret

Конечно, вы хотите, чтобы эти функции были встроенными, я только сделал их не встроенными, чтобы я мог посмотреть, как они компилируются. ( Как убрать "шум" из вывода сборки GCC / clang? ).


Кстати, определение MAT4_MATH__USE_SSE может изменить ABI, если вы когда-либо используете этот союз как член более широкой структуры. struct {int foo; my_mat4 m4; }; требуется 12 байтов заполнения, если my_mat4 выровнено, или нет заполнения в противном случае.

Если вы скомпилируете немного C с и немного C без макроса def, вы можете сделать что-то вроде этого (, если вы решили проблему заставить Python выравнивать объекты ):

#include <stdalign.h>

// give the same alignment regardless of whether the macro is defined.
typedef union my_mat4
{
    alignas(16) float Elements[4][4];
#ifdef MAT4_MATH__USE_SSE
    __m128 Rows[4];
#endif
} my_mat4;

Или нет, если вы не хотите гарантировать выравнивание, когда макрос не определен.

...