Ускорение зависимости от размера данных с помощью автоматической векторизации и sse

Я пытаюсь ускорить некоторый код, используя автоматическую векторизацию из компилятора Intel и используя sse. Все вычисления представляют собой преобразование некоторой структуры node_t в другую структуру w_t (функции tr() и gen_tr()). Когда я пытаюсь векторизовать функцию gen_tr(), она не дает никаких эффектов.

Если изменить формат хранения данных, когда каждый компонент структуры хранится в разных массивах с плавающей запятой, то автовекторизация работает хорошо, см. функцию genv_tr().

Функция, которая использовала sse, вызвала ssev_tr (N должно делиться на 4).

преобразование.с:

#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include <xmmintrin.h>

static __inline__ unsigned long getCC(void)
{
    unsigned a, d;
    asm volatile("rdtsc" : "=a" (a), "=d" (d));
    return ((unsigned long)a) | (((unsigned long)d) << 32);
}

typedef struct {
    float x1, x2, x3, x4, x5;
} node_t;

typedef struct {
    float w1, w2, w3, w4;
} w_t;

void tr(node_t *n, float c1, float c2, w_t *w)
{
    const float nv = n->x1;
    const float N00T = n->x3 * c1;

    const float n1v = n->x2;
    const float N01T = n->x4 * c2;

    w->w1 = nv  - N00T;
    w->w2 = nv  + N00T;
    w->w3 = n1v - N01T;
    w->w4 = n1v + N01T;
}

__attribute__ ((noinline))
void gen_tr(node_t *n, w_t *w, const int N, float c1, float c2)
{
    int i;
    #pragma vector aligned
    #pragma ivdep
    for (i = 0; i < N; i++) {
        tr(n + i, c1, c2, w + i);
    }
}

__attribute__ ((noinline))
void genv_tr(float *x1, float *x2, float *x3, float *x4, float *x5, float *w1, float *w2, float *w3, float *w4, const int N, float c1, float c2)
{
    int i;
    #pragma vector aligned
    #pragma ivdep
    for (i = 0; i < N; i++) {
        const float N00T = x3[i] * c1;
        const float N01T = x4[i] * c2;

        w1[i] = x1[i] - N00T;
        w2[i] = x1[i] + N00T;
        w3[i] = x2[i] - N01T;
        w4[i] = x2[i] + N01T;
    }
}

__attribute__ ((noinline))
void ssev_tr(float *x1, float *x2, float *x3, float *x4, float *x5, float *w1, float *w2, float *w3, float *w4, const int N, float c1, float c2)
{
    __m128 *ws1 = (__m128*)w1;
    __m128 *ws2 = (__m128*)w2;
    __m128 *ws3 = (__m128*)w3;
    __m128 *ws4 = (__m128*)w4;
    
    __m128 *xs1 = (__m128*)x1;
    __m128 *xs2 = (__m128*)x2;
    __m128 *xs3 = (__m128*)x3;
    __m128 *xs4 = (__m128*)x4;
    
    const __m128 cs1 = _mm_set1_ps(c1);
    const __m128 cs2 = _mm_set1_ps(c2);
    
    int i;
    #pragma vector aligned
    #pragma ivdep
    for (i = 0; i < N / 4; i++) {
        const __m128 N00T = _mm_mul_ps(xs3[i], cs1);
        const __m128 N01T = _mm_mul_ps(xs4[i], cs2);

        ws1[i] = _mm_sub_ps(xs1[i], N00T);
        ws2[i] = _mm_add_ps(xs1[i], N00T);
        ws3[i] = _mm_sub_ps(xs2[i], N01T);
        ws4[i] = _mm_add_ps(xs2[i], N01T);
    }
}

#define test(func) \
    for (i = 0; i < n; i++) { \
        x[i].x1 = 1.0; \
        x[i].x2 = 2.0; \
        x[i].x3 = 2.0; \
        x[i].x4 = 2.0; \
        x[i].x5 = 2.0; \
    } \
    \
    t1 = getCC(); \
    for (i = 0; i < rep; i++) { \
        func(x, w, n, c1, c2); \
    } \
    t2 = getCC(); \
    printf("\t%f", ((double)(t2 - t1)) / n / rep);

#define test1(func) \
    for (i = 0; i < n; i++) { \
        x1[i] = 1.0; \
        x2[i] = 2.0; \
        x3[i] = 2.0; \
        x4[i] = 2.0; \
        x5[i] = 2.0; \
    } \
    \
    t1 = getCC(); \
    for (i = 0; i < rep; i++) { \
        func(x1, x2, x3, x4, x5, w1, w2, w3, w4, n, c1, c2); \
    } \
    t2 = getCC(); \
    printf("\t%f", ((double)(t2 - t1)) / n / rep);

int main(int argc, char *argv[])
{
    if (argc < 2) {
        printf("Usage %s vector_size\n", argv[0]);
    }
    int n = atoi(argv[1]);
    printf("%d", n);
    int rep = 100000000 / n;
    int i;
    int inc = 1;
    float c1 = 2.0, c2 = 1.0;
    unsigned long t1, t2;
    node_t *x = (node_t*)malloc(n * sizeof(node_t));
    w_t *w = (w_t*)malloc(n * sizeof(w_t));
    
    float *x1 = (float*)malloc(n * sizeof(float));
    float *x2 = (float*)malloc(n * sizeof(float));
    float *x3 = (float*)malloc(n * sizeof(float));
    float *x4 = (float*)malloc(n * sizeof(float));
    float *x5 = (float*)malloc(n * sizeof(float));
    
    float *w1 = (float*)malloc(n * sizeof(float));
    float *w2 = (float*)malloc(n * sizeof(float));
    float *w3 = (float*)malloc(n * sizeof(float));
    float *w4 = (float*)malloc(n * sizeof(float));
    
    test(gen_tr);
    test1(genv_tr);
    test1(ssev_tr);
    
    printf("\n");
    return 0;
}

Варианты компиляции: icc -O3 -Wall -W -vec-report6 ​​transform.c -o transform

Версия icc - 12.1.2, ОС - Fedora 16 x86_64, ЦП - Intel Core2 Quad CPU Q8200.

Затем я запускаю его с разным размером от 16 до 3000 с шагом 64, здесь скрипт:

#!/bin/bash

echo "" > run.log

for ((c=16;c<3000;c+=64))
do
./transform $c | tee -a run.log
done

Вот некоторый результат работы этого скрипта (размер, gen_tr, genv_tr, ssev_tr), все времена указаны для одного элемента массива:

16      7.710743        3.168577        3.253829
272     7.166493        1.983918        2.618569
528     7.121866        1.920195        2.567109
784     7.115007        1.899451        2.549645
1040    8.104026        2.481062        2.944317
1296    8.137537        5.105032        5.104614
1552    8.118534        5.068812        5.064211
1808    8.138309        5.077831        5.085015
2064    8.149699        5.107503        5.069958
2320    8.164556        5.080981        5.099313
2576    8.151524        5.086056        5.089294
2832    8.212946        5.061927        5.072261

почему так существенно меняется размер 1000 при использовании векторизованной версии функции? Это из-за промаха кеша? Можно ли сохранить одинаковую скорость для всех диапазонов данных?


person Nik0las    schedule 13.02.2012    source источник


Ответы (1)


У вас есть 8 массивов с плавающей запятой. Когда они имеют размер 1000, ваш тест обрабатывает около 32 КБ данных. Даже если ваш кеш L1, вероятно, немного больше (64 КБ), кеш L1, вероятно, не сможет одновременно хранить все данные размером 32 КБ из-за ассоциативности.

Ваш тест повторяется, обрабатывая одни и те же данные снова и снова. Рассмотрим два случая:

  • Размер = 528: 8 массивов удобно помещаются в кэш L1. Каждая итерация теста (кроме первой) имеет быстрый доступ к данным.
  • Размер = 1268: 8 массивов не помещаются в кэш L1 одновременно. Каждая тестовая итерация продолжает вытеснять данные из L1, поэтому фактически все операции чтения и записи переходят в L2.

Таким образом, скачок при размере ввода 1000 частично является артефактом вашего теста, но не полностью. В реальном мире, если у вас уже есть все необходимые данные в кеше L1, genv_tr будет очень быстрым. Но на входах размером >1000 все входы просто не помещаются в кеш L1, поэтому часть обращений точно пойдет в L2.

person Igor ostrovsky    schedule 14.02.2012