Memset параллельно с потоками, связанными с каждым физическим ядром

Я тестировал код В параллельном коде OpenMP было ли какое-либо преимущество для параллельного выполнения memset?, и я наблюдаю что-то неожиданное.

Моя система - это один сокет Xeon E5-1620, который является процессором Ivy Bridge с 4 физическими ядрами и восемью гиперпотоками. Я использую Ubuntu 14.04 LTS, Linux Kernel 3.13, GCC 4.9.0 и EGLIBC 2.19. Я скомпилирую gcc -fopenmp -O3 mem.c

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

Touch:   11830.448 MB/s
Rewrite: 18133.428 MB/s

Однако, когда я связываю потоки и устанавливаю количество потоков на число физических ядер, таких как

export OMP_NUM_THREADS=4 
export OMP_PROC_BIND=true

Я получаю

Touch:   22167.854 MB/s
Rewrite: 18291.134 MB/s

Скорость нажатия удваивается! Запуск несколько раз после привязки всегда имеет контакт быстрее, чем переписывать. Я этого не понимаю. Почему Touch быстрее, чем переписывать после привязки потоков и установки их на количество физических ядер? Почему скорость нажатия удвоилась?

Вот код, который я использовал без изменений из ответа Кристо Илиева.

#include <stdio.h>
#include <string.h>
#include <omp.h>

void zero(char *buf, size_t size)
{
    size_t my_start, my_size;

    if (omp_in_parallel())
    {
        int id = omp_get_thread_num();
        int num = omp_get_num_threads();

        my_start = (id*size)/num;
        my_size = ((id+1)*size)/num - my_start;
    }
    else
    {
        my_start = 0;
        my_size = size;
    }

    memset(buf + my_start, 0, my_size);
}

int main (void)
{
    char *buf;
    size_t size = 1L << 31; // 2 GiB
    double tmr;

    buf = malloc(size);

    // Touch
    tmr = -omp_get_wtime();
    #pragma omp parallel
    {
        zero(buf, size);
    }
    tmr += omp_get_wtime();
    printf("Touch:   %.3f MB/s\n", size/(1.e+6*tmr));

    // Rewrite
    tmr = -omp_get_wtime();
    #pragma omp parallel
    {
        zero(buf, size);
    }
    tmr += omp_get_wtime();
    printf("Rewrite: %.3f MB/s\n", size/(1.e+6*tmr));

    free(buf);

    return 0;
}

Изменить: Без привязки протектора, но с использованием четырех потоков здесь приведены результаты, выполняемые восемь раз.

Touch:   14723.115 MB/s, Rewrite: 16382.292 MB/s
Touch:   14433.322 MB/s, Rewrite: 16475.091 MB/s 
Touch:   14354.741 MB/s, Rewrite: 16451.255 MB/s  
Touch:   21681.973 MB/s, Rewrite: 18212.101 MB/s 
Touch:   21004.233 MB/s, Rewrite: 17819.072 MB/s 
Touch:   20889.179 MB/s, Rewrite: 18111.317 MB/s 
Touch:   14528.656 MB/s, Rewrite: 16495.861 MB/s
Touch:   20958.696 MB/s, Rewrite: 18153.072 MB/s

Edit:

Я тестировал этот код на двух других системах, и я не могу воспроизвести проблему на них

i5-4250U (Haswell) - 2 физических ядра, 4 гиперпотока

4 threads unbound
    Touch:   5959.721 MB/s, Rewrite: 9524.160 MB/s
2 threads bound to each physical core
    Touch:   7263.175 MB/s, Rewrite: 9246.911 MB/s

Четыре гнезда E7- 4850 - 10 физических ядер, 20 гиперпотоков каждый разъем

80 threads unbound
    Touch:   10177.932 MB/s, Rewrite: 25883.520 MB/s
40 threads bound
    Touch:   10254.678 MB/s, Rewrite: 30665.935 MB/s

Это показывает, что привязка потоков к физическим ядрам улучшает как прикосновение, так и переписывание, но прикосновение происходит медленнее, чем переписывать эти две системы.

Я также проверил три разных варианта memset: my_memset, my_memset_stream и A_memset. Функции my_memset и my_memset_stream определены ниже. Функция A_memset происходит от Agner Fog asmlib.

Результаты my_memset:

Touch:   22463.186 MB/s
Rewrite: 18797.297 MB/s

Я думаю, это показывает, что проблема не в функции memset EGLIBC.

Результаты A_memset:

Touch:   18235.732 MB/s
Rewrite: 44848.717 MB/s

my_memset_stream:

Touch:   18678.841 MB/s
Rewrite: 44627.270 MB/s

Посмотрев на исходный код asmlib, я увидел, что для записи больших фрагментов памяти используются не временные хранилища. Вот почему my_memset_stream получает примерно такую ​​же пропускную способность, что и Agner Fog asmlib. максимальная пропускная способность этой системы составляет 51,2 ГБ/с. Таким образом, это показывает, что A_memset и my_memset_stream получают около 85% от максимальной пропускной способности.

void my_memset(int *s, int c, size_t n) {
    int i;
    for(i=0; i<n/4; i++) {
        s[i] = c;
    }
}

void my_memset_stream(int *s, int c, size_t n) {
    int i;
    __m128i v = _mm_set1_epi32(c);

    for(i=0; i<n/4; i+=4) {
        _mm_stream_si128((__m128i*)&s[i], v);
    }
}

Ответы

Ответ 1

Из ваших чисел видно, что ваши 4 связанные потоки работают на 2 физических ядрах вместо ожидаемых 4 физических ядер. Можете ли вы это подтвердить? Это объясняет удвоение времени Touch. Я не уверен, как заставить поток к физическому ядру при использовании hyperthreading в вашей системе. {Я попытался добавить это как вопрос, но у меня недостаточно "репутации" }