Измерение пропускной способности памяти от точечного произведения двух массивов

Точечное произведение двух массивов

for(int i=0; i<n; i++) {
sum += x[i]*y[i];
}

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

Используя код на
почему-векторизации-The-петля-делает-не-иметь-производительность улучшение Я получаю пропускную способность 9,3 ГБ / с для моей системы. Тем не менее, когда я пытаюсь вычислить пропускную способность, используя точечный продукт, я получаю более чем вдвое большую скорость для одного потока и более трех раз скорость, используя несколько потоков (моя система имеет четыре ядра / восемь гиперпотоков). Это не имеет смысла для меня, так как операция, связанная с памятью, не должна выигрывать от нескольких потоков. Вот вывод из кода ниже:

Xeon E5-1620, GCC 4.9.0, Linux kernel 3.13
dot 1 thread:      1.0 GB, sum 191054.81, time 4.98 s, 21.56 GB/s, 5.39 GFLOPS
dot_avx 1 thread   1.0 GB, sum 191043.33, time 5.16 s, 20.79 GB/s, 5.20 GFLOPS
dot_avx 2 threads: 1.0 GB, sum 191045.34, time 3.44 s, 31.24 GB/s, 7.81 GFLOPS
dot_avx 8 threads: 1.0 GB, sum 191043.34, time 3.26 s, 32.91 GB/s, 8.23 GFLOPS

Может кто-нибудь объяснить мне, почему я получаю более чем вдвое большую пропускную способность для одного потока и более чем в три раза большую пропускную способность, используя более одного потока?

Вот код, который я использовал:

//g++ -O3 -fopenmp -mavx -ffast-math dot.cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <x86intrin.h>
#include <omp.h>

extern "C" inline float horizontal_add(__m256 a) {
__m256 t1 = _mm256_hadd_ps(a,a);
__m256 t2 = _mm256_hadd_ps(t1,t1);
__m128 t3 = _mm256_extractf128_ps(t2,1);
__m128 t4 = _mm_add_ss(_mm256_castps256_ps128(t2),t3);
return _mm_cvtss_f32(t4);
}

extern "C" float dot_avx(float * __restrict x, float * __restrict y, const int n) {
x = (float*)__builtin_assume_aligned (x, 32);
y = (float*)__builtin_assume_aligned (y, 32);
float sum = 0;
#pragma omp parallel reduction(+:sum)
{
__m256 sum1 = _mm256_setzero_ps();
__m256 sum2 = _mm256_setzero_ps();
__m256 sum3 = _mm256_setzero_ps();
__m256 sum4 = _mm256_setzero_ps();
__m256 x8, y8;
#pragma omp for
for(int i=0; i<n; i+=32) {
x8 = _mm256_loadu_ps(&x[i]);
y8 = _mm256_loadu_ps(&y[i]);
sum1 = _mm256_add_ps(_mm256_mul_ps(x8,y8),sum1);
x8 = _mm256_loadu_ps(&x[i+8]);
y8 = _mm256_loadu_ps(&y[i+8]);
sum2 = _mm256_add_ps(_mm256_mul_ps(x8,y8),sum2);
x8 = _mm256_loadu_ps(&x[i+16]);
y8 = _mm256_loadu_ps(&y[i+16]);
sum3 = _mm256_add_ps(_mm256_mul_ps(x8,y8),sum3);
x8 = _mm256_loadu_ps(&x[i+24]);
y8 = _mm256_loadu_ps(&y[i+24]);
sum4 = _mm256_add_ps(_mm256_mul_ps(x8,y8),sum4);
}
sum += horizontal_add(_mm256_add_ps(_mm256_add_ps(sum1,sum2),_mm256_add_ps(sum3,sum4)));
}
return sum;
}

extern "C" float dot(float * __restrict x, float * __restrict y, const int n) {
x = (float*)__builtin_assume_aligned (x, 32);
y = (float*)__builtin_assume_aligned (y, 32);
float sum = 0;
for(int i=0; i<n; i++) {
sum += x[i]*y[i];
}
return sum;
}

int main(){
uint64_t LEN = 1 << 27;
float *x = (float*)_mm_malloc(sizeof(float)*LEN,64);
float *y = (float*)_mm_malloc(sizeof(float)*LEN,64);
for(uint64_t i=0; i<LEN; i++) { x[i] = 1.0*rand()/RAND_MAX - 0.5; y[i] = 1.0*rand()/RAND_MAX - 0.5;}

uint64_t size = 2*sizeof(float)*LEN;

volatile float sum = 0;
double dtime, rate, flops;
int repeat = 100;

dtime = omp_get_wtime();
for(int i=0; i<repeat; i++) sum += dot(x,y,LEN);
dtime = omp_get_wtime() - dtime;
rate = 1.0*repeat*size/dtime*1E-9;
flops = 2.0*repeat*LEN/dtime*1E-9;
printf("%f GB, sum %f, time %f s, %.2f GB/s, %.2f GFLOPS\n", 1.0*size/1024/1024/1024, sum, dtime, rate,flops);

sum = 0;
dtime = omp_get_wtime();
for(int i=0; i<repeat; i++) sum += dot_avx(x,y,LEN);
dtime = omp_get_wtime() - dtime;
rate = 1.0*repeat*size/dtime*1E-9;
flops = 2.0*repeat*LEN/dtime*1E-9;

printf("%f GB, sum %f, time %f s, %.2f GB/s, %.2f GFLOPS\n", 1.0*size/1024/1024/1024, sum, dtime, rate,flops);
}

Я только что скачал, выполнил и запустил STREAM в соответствии с предложением Джонатана Дурси, и вот результаты:

Одна нить

Function      Rate (MB/s)   Avg time     Min time     Max time
Copy:       14292.1657       0.0023       0.0022       0.0023
Scale:      14286.0807       0.0023       0.0022       0.0023
Add:        14724.3906       0.0033       0.0033       0.0033
Triad:      15224.3339       0.0032       0.0032       0.0032

Восемь нитей

Function      Rate (MB/s)   Avg time     Min time     Max time
Copy:       24501.2282       0.0014       0.0013       0.0021
Scale:      23121.0556       0.0014       0.0014       0.0015
Add:        25263.7209       0.0024       0.0019       0.0056
Triad:      25817.7215       0.0020       0.0019       0.0027

19

Решение

Здесь происходит несколько вещей, которые сводятся к:

  • Вы должны работать довольно усердно, чтобы получить все до последней части производительности подсистемы памяти; а также
  • Различные тесты измеряют разные вещи.

Первый помогает объяснить, почему вам нужно несколько потоков для насыщения доступной пропускной способности памяти. В системе памяти много параллелизма, и для его использования часто требуется некоторый параллелизм в коде вашего процессора. Одна большая причина, по которой помогают несколько потоков выполнения — скрытие латентности — в то время как один поток останавливается в ожидании поступления данных, другой поток может использовать некоторые другие данные, которые только что стали доступны.

Аппаратное обеспечение очень помогает вам в одном потоке в этом случае — поскольку доступ к памяти настолько предсказуем, аппаратное обеспечение может предварительно выбирать данные заранее, когда вам это нужно, что дает вам некоторые преимущества скрытия задержки даже в одном потоке; но есть пределы тому, что может делать предварительная выборка. Например, средство предварительной выборки не возьмет на себя обязательство пересекать границы страницы. Каноническая ссылка для большей части этого Что каждый программист должен знать о памяти Ульриха Дреппера, который уже достаточно стар, чтобы начали появляться некоторые пробелы (краткий обзор Intel для вашего процессора Sandy Bridge Вот — обратите внимание, в частности, на более тесную интеграцию оборудования управления памятью с процессором).

Что касается вопроса о сравнении с memset, MBW или же ПОТОК, сравнение между тестами всегда вызывает головную боль, даже тесты, которые утверждают, что измеряют одно и то же. В частности, «пропускная способность памяти» — это не одно число — производительность варьируется в зависимости от операций. И mbw, и Stream выполняют некоторую версию операции копирования, причем здесь прописаны операции STREAM (взятые прямо с веб-страницы, все операнды с плавающей запятой двойной точности):

------------------------------------------------------------------
name        kernel                  bytes/iter      FLOPS/iter
------------------------------------------------------------------
COPY:       a(i) = b(i)                 16              0
SCALE:      a(i) = q*b(i)               16              1
SUM:        a(i) = b(i) + c(i)          24              1
TRIAD:      a(i) = b(i) + q*c(i)        24              2
------------------------------------------------------------------

Таким образом, примерно 1 / 2-1 / 3 операций с памятью в этих случаях являются записью (и все это запись в случае memset). В то время как отдельные записи могут быть немного медленнее, чем чтения, большая проблема в том, что насыщать подсистему памяти записями гораздо сложнее, потому что, конечно, вы не можете сделать эквивалент предварительной загрузки записи. Чередование операций чтения и записи помогает, но ваш пример с точечным произведением, который по существу представляет собой все операции чтения, будет о наилучшем возможном случае привязки иглы к пропускной способности памяти.

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

12

Другие решения

Я сделал свой собственный код теста памяти https://github.com/zboson/bandwidth

Вот текущие результаты для восьми потоков:

write:    0.5 GB, time 2.96e-01 s, 18.11 GB/s
copy:       1 GB, time 4.50e-01 s, 23.85 GB/s
scale:      1 GB, time 4.50e-01 s, 23.85 GB/s
add:      1.5 GB, time 6.59e-01 s, 24.45 GB/s
mul:      1.5 GB, time 6.56e-01 s, 24.57 GB/s
triad:    1.5 GB, time 6.61e-01 s, 24.37 GB/s
vsum:     0.5 GB, time 1.49e-01 s, 36.09 GB/s, sum -8.986818e+03
vmul:     0.5 GB, time 9.00e-05 s, 59635.10 GB/s, sum 0.000000e+00
vmul_sum:   1 GB, time 3.25e-01 s, 33.06 GB/s, sum 1.910421e+04

Вот результаты токов для 1 потока:

write:    0.5 GB, time 4.65e-01 s, 11.54 GB/s
copy:       1 GB, time 7.51e-01 s, 14.30 GB/s
scale:      1 GB, time 7.45e-01 s, 14.41 GB/s
add:      1.5 GB, time 1.02e+00 s, 15.80 GB/s
mul:      1.5 GB, time 1.07e+00 s, 15.08 GB/s
triad:    1.5 GB, time 1.02e+00 s, 15.76 GB/s
vsum:     0.5 GB, time 2.78e-01 s, 19.29 GB/s, sum -8.990941e+03
vmul:     0.5 GB, time 1.15e-05 s, 468719.08 GB/s, sum 0.000000e+00
vmul_sum:   1 GB, time 5.72e-01 s, 18.78 GB/s, sum 1.910549e+04
  1. write: записывает константу (3.14159) в массив. Это должно быть как memset,
  2. copy, scale, add и triad определены так же, как в STREAM
  3. мул: a(i) = b(i) * c(i)
  4. ВСУМ: sum += a(i)
  5. vmul: sum *= a(i)
  6. vmul_sum: sum += a(i)*b(i) // точечный продукт

Мои результаты соответствуют STREAM. Я получаю самую высокую пропускную способность для vsum, vmul метод не работает в настоящее время (если значение равно нулю, оно заканчивается рано). Я могу получить немного лучшие результаты (примерно на 10%), используя встроенные функции и развернув цикл, который я добавлю позже.

3

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector