2016-05-30 9 views
9

Я генерирую инструкции sse/avx, и в настоящее время мне приходится использовать нестандартные нагрузки и магазины. Я работаю с массивом float/double, и я никогда не узнаю, будет ли он выровнен или нет. Поэтому, прежде чем векторизовать его, я хотел бы иметь pre и, возможно, цикл post, который заботится о негласной части. Основной векторный цикл действует тогда на выровненную часть.Обработать невыровненную часть двойного массива, векторизовать остальные

Но как определить, когда массив выровнен? Могу ли я проверить значение указателя? Когда должна быть остановлена ​​прецизионная остановка и запуск после цикла?

Вот мой простой пример кода:

void func(double * in, double * out, unsigned int size){ 
    for(as long as in unaligned part){ 
     out[i] = do_something_with_array(in[i]) 
    } 
    for(as long as aligned){ 
     awesome avx code that loads operates and stores 4 doubles 
    } 
    for(remaining part of array){ 
     out[i] = do_something_with_array(in[i]) 
    } 
} 

Edit: Я думал об этом. Теоретически указатель на i-й элемент должен быть делящимся (что-то вроде & a [i]% 16 == 0) на 2,4,16,32 (в зависимости от того, является ли он двойным и является ли он sse или avx). Таким образом, первый цикл должен покрывать элементы, которые не могут быть делящимися.

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

+2

Вы смотрели на код авто-векторизации компилятор, как '' gcc' или clang' будет производить дело с возможной несоосностью? Я подозреваю, что вы не совсем знаете, в чем вы попадаете. – EOF

+1

Да, разложение массива на указатель и выполнение арифметики на нем поможет вам определить, находится ли он на хорошем выровненном адресе или нет. * Но ... * Проверка этого не поможет. Если адрес первого элемента не будет хорошо выровнен, адреса в * все * элементы могут быть неровными или, по крайней мере, многими или большинством элементов. Лучшей идеей может быть использование динамического распределения и добавление начального заполнения, чтобы убедиться, что у вас выровненный «массив». –

+2

@JoachimPileborg: Я думал, что речь идет о смещении данных для векторных команд, и в этом случае отдельные * элементы * вектора * выравниваются *. – EOF

ответ

5

Вот несколько примеров кода C, который делает то, что вы хотите

#include <stdio.h> 
#include <x86intrin.h> 
#include <inttypes.h> 

#define ALIGN 32 
#define SIMD_WIDTH (ALIGN/sizeof(double)) 

int main(void) { 
    int n = 17; 
    int c = 1; 
    double* p = _mm_malloc((n+c) * sizeof *p, ALIGN); 
    double* p1 = p+c; 
    for(int i=0; i<n; i++) p1[i] = 1.0*i; 
    double* p2 = (double*)((uintptr_t)(p1+SIMD_WIDTH-1)&-ALIGN); 
    double* p3 = (double*)((uintptr_t)(p1+n)&-ALIGN); 
    if(p2>p3) p2 = p3; 

    printf("%p %p %p %p\n", p1, p2, p3, p1+n); 
    double *t; 
    for(t=p1; t<p2; t+=1) { 
     printf("a %p %f\n", t, *t); 
    } 
    puts(""); 
    for(;t<p3; t+=SIMD_WIDTH) { 
     printf("b %p ", t); 
     for(int i=0; i<SIMD_WIDTH; i++) printf("%f ", *(t+i)); 
     puts(""); 
    } 
    puts(""); 
    for(;t<p1+n; t+=1) { 
     printf("c %p %f\n", t, *t); 
    } 
} 

Это создает 32-байтовый выровненный буфер а а затем сдвигается на один двойной в размере, так что это больше не 32-байтовый выровнены. Он перескакивает через скалярные значения до достижения 32-битного выравнивания, обходит по 32-байтовым выровненным значениям и затем заканчивает другим скалярным циклом для любых оставшихся значений, которые не являются кратными ширине SIMD.


Я бы сказал, что такая оптимизация действительно имеет большое значение для процессоров Intel x86 до Nehalem. Так как Nehalem латентность и пропускная способность неуравновешенных нагрузок и хранилищ такие же, как для выровненных нагрузок и хранилищ. Кроме того, поскольку Nehalem затраты на разделение строк кэша малы.

Существует одна тонкая точка с SSE, так как Nehalem в том, что неуравновешенные нагрузки и магазины не могут складываться с другими операциями. Поэтому выровненные нагрузки и хранилища не устаревают с SSE с Nehalem. Поэтому в принципе эта оптимизация может иметь значение даже с Nehalem, но на практике я думаю, что в этом случае мало случаев.

Тем не менее, при несогласованных нагрузках и хранилищах AVX могут складываться, поэтому выровненные нагрузки и инструкции хранения устарели.


I looked into this with GCC, MSVC, and Clang. GCC, если он не может принять указатель, выравнивается, например, 16 байтов с SSE, тогда он будет генерировать код, аналогичный приведенному выше коду, чтобы достичь выравнивания по 16 байт, чтобы избежать расщепления строки кэша при векторизации.

Clang и MSVC не делают этого, чтобы пострадали от расщепления линии кэширования. Тем не менее, стоимость дополнительного кода для этого компенсирует затраты на разделение кэша, что, вероятно, объясняет, почему Clang и MSVC не беспокоятся об этом.

Исключение составляет только Nahalem. В этом случае GCC намного быстрее, чем Clang и MSVC, когда указатель не выровнен. Если указатель выровнен и Clang знает его, он будет использовать выровненные нагрузки и магазины и быть быстрым, как GCC.В векторизации MSVC по-прежнему используются неустановленные хранилища и нагрузки и, следовательно, медленный pre-Nahalem, даже если указатель выравнивается по 16 байт.


Вот вариант, который я думаю, что это немного яснее, используя различия указателей

#include <stdio.h> 
#include <x86intrin.h> 
#include <inttypes.h> 

#define ALIGN 32 
#define SIMD_WIDTH (ALIGN/sizeof(double)) 

int main(void) { 
    int n = 17, c =1; 

    double* p = _mm_malloc((n+c) * sizeof *p, ALIGN); 
    double* p1 = p+c; 
    for(int i=0; i<n; i++) p1[i] = 1.0*i; 
    double* p2 = (double*)((uintptr_t)(p1+SIMD_WIDTH-1)&-ALIGN); 
    double* p3 = (double*)((uintptr_t)(p1+n)&-ALIGN); 
    int n1 = p2-p1, n2 = p3-p2; 
    if(n1>n2) n1=n2; 
    printf("%d %d %d\n", n1, n2, n); 

    int i; 
    for(i=0; i<n1; i++) { 
     printf("a %p %f\n", &p1[i], p1[i]); 
    } 
    puts(""); 
    for(;i<n2; i+=SIMD_WIDTH) { 
     printf("b %p ", &p1[i]); 
     for(int j=0; j<SIMD_WIDTH; j++) printf("%f ", p1[i+j]); 
     puts(""); 
    } 
    puts(""); 
    for(;i<n; i++) { 
     printf("c %p %f\n", &p1[i], p1[i]); 
    } 
} 
+0

Я обязательно попробую это. Мне кажется очень разумным. Одно замечание к вашему обсуждению оптимизации. Afaik вам нужно выровнять нагрузки/хранить, если вы хотите использовать инструкцию потоковой передачи, например. Невременный магазин. – hr0m

+0

@ hr0m: Правильно, магазины NT доступны только в выровненных версиях. Обычно вы этого не хотите, потому что они принудительно выселяют пункт назначения из кеша. Это плохо, если вы собираетесь повторно использовать данные позже. Нагрузки NT не делают ничего другого в обычной (обратной) памяти, на текущих микроархитектурах. На текущих процессорах нагрузки NT полезны только при загрузке из памяти WC (например, видеопамяти). –

+0

Вы не упомянули о главном усложнении для OP: если вход и выход имеют разные выравнивания, единственный способ сделать выравниваемые нагрузки и магазины - 'palignr'. (Не стоит на Нехалеме и позже). –

Смежные вопросы