2016-12-03 11 views
0

Я пытаюсь ускорить работу одной программы с помощью предварительной выборки. Цель моей программы - просто проверить. Вот то, что она делает:Ускорьте случайный доступ к памяти с использованием предварительной выборки

  1. Он использует два Int буферов одного и того же размера
  2. Он читает один за другим все значения первого буфера
  3. Он считывает значение в индексе во втором буфере
  4. Он суммирует все значения, взятые из второго буфера
  5. Это все предыдущие шаги для большего и большего
  6. в конце я печатаю число и непроизвольного CP У

В первый раз, значение в первых буферах содержит значение его индекса (ср функция createIndexBuffer в коде чуть ниже).

Это будет более понятно в коде моей программы:

#include <stdio.h> 
#include <stdlib.h> 
#include <limits.h> 
#include <sys/time.h> 

#define BUFFER_SIZE ((unsigned long) 4096 * 100000) 


unsigned int randomUint() 
{ 
    int value = rand() % UINT_MAX; 
    return value; 
} 


unsigned int * createValueBuffer() 
{ 
    unsigned int * valueBuffer = (unsigned int *) malloc(BUFFER_SIZE * sizeof(unsigned int)); 
    for (unsigned long i = 0 ; i < BUFFER_SIZE ; i++) 
    { 
    valueBuffer[i] = randomUint(); 
    } 

    return (valueBuffer); 
} 


unsigned int * createIndexBuffer() 
{ 
    unsigned int * indexBuffer = (unsigned int *) malloc(BUFFER_SIZE * sizeof(unsigned int)); 
    for (unsigned long i = 0 ; i < BUFFER_SIZE ; i++) 
    { 
    indexBuffer[i] = i; 
    } 

    return (indexBuffer); 
} 


unsigned long long computeSum(unsigned int * indexBuffer, unsigned int * valueBuffer) 
{ 
    unsigned long long sum = 0; 

    for (unsigned int i = 0 ; i < BUFFER_SIZE ; i++) 
    { 
    unsigned int index = indexBuffer[i]; 
    sum += valueBuffer[index]; 
    } 

    return (sum); 
} 


unsigned int computeTimeInMicroSeconds() 
{ 
    unsigned int * valueBuffer = createValueBuffer(); 
    unsigned int * indexBuffer = createIndexBuffer(); 

    struct timeval startTime, endTime; 
    gettimeofday(&startTime, NULL); 

    unsigned long long sum = computeSum(indexBuffer, valueBuffer); 

    gettimeofday(&endTime, NULL); 

    printf("Sum = %llu\n", sum); 
    free(indexBuffer); 
    free(valueBuffer); 

    return ((endTime.tv_sec - startTime.tv_sec) * 1000 * 1000) + (endTime.tv_usec - startTime.tv_usec); 

} 


int main() 
{ 
    printf("sizeof buffers = %ldMb\n", BUFFER_SIZE * sizeof(unsigned int)/(1024 * 1024)); 
    unsigned int timeInMicroSeconds = computeTimeInMicroSeconds(); 
    printf("Time: %u micro-seconds = %.3f seconds\n", timeInMicroSeconds, (double) timeInMicroSeconds/(1000 * 1000)); 
} 

Если я запускаю его, я получаю следующий результат:

$ gcc TestPrefetch.c -O3 -o TestPrefetch && ./TestPrefetch 
sizeof buffers = 1562Mb 
Sum = 439813150288855829 
Time: 201172 micro-seconds = 0.201 seconds 

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

Мы можем сделать его более сложным, чтобы данные (почти) были предварительно обработаны в кэше ЦП. Например, мы можем просто изменить функцию createIndexBuffer в:

unsigned int * createIndexBuffer() 
{ 
    unsigned int * indexBuffer = (unsigned int *) malloc(BUFFER_SIZE * sizeof(unsigned int)); 
    for (unsigned long i = 0 ; i < BUFFER_SIZE ; i++) 
    { 
    indexBuffer[i] = rand() % BUFFER_SIZE; 
    } 

    return (indexBuffer); 
} 

Давайте попробуем программу еще раз:

$ gcc TestPrefetch.c -O3 -o TestPrefetch && ./TestPrefetch 
sizeof buffers = 1562Mb 
Sum = 439835307963131237 
Time: 3730387 micro-seconds = 3.730 seconds 

Более 18 раз медленнее !!!

Теперь мы приходим к моей проблеме. С учетом новой createIndexBuffer функции, я хотел бы ускорить computeSum функцию с помощью PreFetch

unsigned long long computeSum(unsigned int * indexBuffer, unsigned int * valueBuffer) 
{ 
    unsigned long long sum = 0; 

    for (unsigned int i = 0 ; i < BUFFER_SIZE ; i++) 
    { 
    __builtin_prefetch((char *) &indexBuffer[i + 1], 0, 0); 
    unsigned int index = indexBuffer[i]; 
    sum += valueBuffer[index]; 
    } 

    return (sum); 
} 

, конечно, я также должен изменить мой createIndexBuffer, чтобы он выделяет буфер, имеющий еще один элемент

I программе возобновления моя программа: не лучше! Как упреждение может быть медленнее, чем один «для» итерации цикла, я могу упреждение не один элемента раньше, но два элемента, прежде чем

__builtin_prefetch((char *) &indexBuffer[i + 2], 0, 0); 

не лучше! две петли итераций? не лучше? Три? ** Я пробовал это до 50 (!!!), но я не могу повысить производительность своей функции computeSum.

Могу ли я хотел бы помочь понять, почему Большое спасибо за вашу помощь

ответ

3

Я считаю, что приведенный выше код автоматически оптимизируется CPU без дополнительного пространства для ручной оптимизации.

1. Основная проблема заключается в том, что последовательный доступ к indexBuffer. Аппаратный предварительный считыватель воспринимает его и автоматически выбирает дополнительные значения, без необходимости вызывать предварительную выборку вручную. Итак, во время итерации #i значения indexBuffer[i+1], indexBuffer[i+2], ... уже находятся в кеше. (Кстати, нет необходимости добавлять искусственный элемент в конец массива: ошибки доступа к памяти молча игнорируются инструкциями предварительной выборки).

Что вам действительно нужно сделать, это упреждающий valueBuffer вместо:

__builtin_prefetch((char *) &valueBuffer[indexBuffer[i + 1]], 0, 0); 

2. Но добавление выше строки кода не поможет либо в таком простом сценарии. Стоимость доступа к памяти составляет сотни циклов, а команда add - ~ 1 цикл. Ваш код уже тратит 99% времени на доступ к памяти. Добавление ручной предвыборки сделает это на один цикл быстрее и не лучше.

Ручная предварительная выборка будет действительно хорошо работать, если ваша математика будет намного более тяжелой (попробуйте), например, используя выражение с большим количеством неоптимизированных разделов (по 20-30 циклов) или вызовом некоторой математической функции (log, син).

3. Но даже это не гарантирует помощь. Зависимость между итерациями цикла очень слабая, только переменная sum. Это позволяет процессору выполнять спекулятивные инструкции: он может начать выборку valueBuffer[i+1] одновременно, все еще выполняя математику для valueBuffer[i].

+0

Мой ответ для вашего предложения 'sin' выше вашего ответа, а не ниже (я, конечно, допустил ошибку ...) –

0

Извините. То, что я вам дал, было неправильной версией моего кода. Правильная версия, что вы сказали:

__builtin_prefetch((char *) &valueBuffer[indexBuffer[i + prefetchStep]], 0, 0); 

Однако, даже с правильной версии, к сожалению, не лучше

0

Тогда я адаптировал свою программу, чтобы попробовать свои предложения с помощью функции sin.

Моя адаптированная программа заключается в следующем:

#include <stdio.h> 
#include <stdlib.h> 
#include <limits.h> 
#include <sys/time.h> 
#include <math.h> 

#define BUFFER_SIZE ((unsigned long) 4096 * 50000) 


unsigned int randomUint() 
{ 
    int value = rand() % UINT_MAX; 
    return value; 
} 


unsigned int * createValueBuffer() 
{ 
    unsigned int * valueBuffer = (unsigned int *) malloc(BUFFER_SIZE * sizeof(unsigned int)); 
    for (unsigned long i = 0 ; i < BUFFER_SIZE ; i++) 
    { 
    valueBuffer[i] = randomUint(); 
    } 

    return (valueBuffer); 
} 


unsigned int * createIndexBuffer(unsigned short prefetchStep) 
{ 
    unsigned int * indexBuffer = (unsigned int *) malloc((BUFFER_SIZE + prefetchStep) * sizeof(unsigned int)); 
    for (unsigned long i = 0 ; i < BUFFER_SIZE ; i++) 
    { 
    indexBuffer[i] = rand() % BUFFER_SIZE; 
    } 

    return (indexBuffer); 
} 


double computeSum(unsigned int * indexBuffer, unsigned int * valueBuffer, unsigned short prefetchStep) 
{ 
    double sum = 0; 

    for (unsigned int i = 0 ; i < BUFFER_SIZE ; i++) 
    { 
    __builtin_prefetch((char *) &valueBuffer[indexBuffer[i + prefetchStep]], 0, 0); 
    unsigned int index = indexBuffer[i]; 
    sum += sin(valueBuffer[index]); 
    } 

    return (sum); 
} 


unsigned int computeTimeInMicroSeconds(unsigned short prefetchStep) 
{ 
    unsigned int * valueBuffer = createValueBuffer(); 
    unsigned int * indexBuffer = createIndexBuffer(prefetchStep); 

    struct timeval startTime, endTime; 
    gettimeofday(&startTime, NULL); 

    double sum = computeSum(indexBuffer, valueBuffer, prefetchStep); 

    gettimeofday(&endTime, NULL); 

    printf("prefetchStep = %d, Sum = %f - ", prefetchStep, sum); 
    free(indexBuffer); 
    free(valueBuffer); 

    return ((endTime.tv_sec - startTime.tv_sec) * 1000 * 1000) + (endTime.tv_usec - startTime.tv_usec); 

} 


int main() 
{ 
    printf("sizeof buffers = %ldMb\n", BUFFER_SIZE * sizeof(unsigned int)/(1024 * 1024)); 
    for (unsigned short prefetchStep = 0 ; prefetchStep < 250 ; prefetchStep++) 
    { 
    unsigned int timeInMicroSeconds = computeTimeInMicroSeconds(prefetchStep); 
    printf("Time: %u micro-seconds = %.3f seconds\n", timeInMicroSeconds, (double) timeInMicroSeconds/(1000 * 1000)); 
    } 
} 

Выход:

$ gcc TestPrefetch.c -O3 -o TestPrefetch -lm && taskset -c 7 ./TestPrefetch 
sizeof buffers = 781Mb 
prefetchStep = 0, Sum = -1107.523504 - Time: 20895326 micro-seconds = 20.895 seconds 
prefetchStep = 1, Sum = 13456.262424 - Time: 12706720 micro-seconds = 12.707 seconds 
prefetchStep = 2, Sum = -20179.289469 - Time: 12136174 micro-seconds = 12.136 seconds 
prefetchStep = 3, Sum = 12068.302534 - Time: 11233803 micro-seconds = 11.234 seconds 
prefetchStep = 4, Sum = 21071.238160 - Time: 10855348 micro-seconds = 10.855 seconds 
prefetchStep = 5, Sum = -22648.280105 - Time: 10517861 micro-seconds = 10.518 seconds 
prefetchStep = 6, Sum = 22665.381676 - Time: 9205809 micro-seconds = 9.206 seconds 
prefetchStep = 7, Sum = 2461.741268 - Time: 11391088 micro-seconds = 11.391 seconds 
... 

Так вот, он работает лучше! Честно говоря, я был почти уверен, что это будет не лучше, потому что стоимость математической функции выше по сравнению с доступом к памяти.

Если кто-то может дать мне более подробную информацию о том, почему это лучше сейчас, я был бы признателен за это

Большое спасибо

0

Prefetch получает обычно полную строку кэша. Это typically 64 bytes. Таким образом, случайный пример извлекает всегда 64 байта для 4 байтового int. В 16 раз больше данных, которые вам действительно нужны, что очень хорошо подходит для замедления в 18 раз. Таким образом, код просто ограничен пропускной способностью памяти, а не задержкой.