Недавно в процессе выполнения учебного задания мне потребовалось реализовать метод конечных разностей для нахождения приближённого решения краевой задачи. По сути, я впервые столкнулся с вычислениями с плавающей точкой и не мог не попробовать запустить свою программу на Эльбрусе, зная о его больших возможностях и заточенности под вычисления такого рода. Хотите удивиться? Отправляйтесь со мной в увлекательное путешествие!

Вкратце о математике

Для понимания происходящего математическая постановка задачи не является принципиально важной, так что её подробный разбор я позволю себе пропустить и ограничусь коротким описанием.

Итак, рассматривается дифференциальное уравнение Пуассона в прямоугольной области [0; 4] x [0; 3]:

-\Delta u + q(x, y)u = F(x, y),\spaceгде \space \Delta u = \frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 u}{\partial y^2}

Для данного уравнения рассматривается краевая задача, причём на каждом отрезке границы заданы граничные условия третьего типа:

\frac{\partial u}{\partial n}(x, y) + u(x, y) = \psi(x, y),

где n— единичная внешняя нормаль к границе.

Решение u(x, y)будем искать с помощью разностной схемы, которая может быть представлена в виде конечно-разностной операторной задачиA\omega = B. Вычисления представляют из себя итерационный метод, на каждом шаге kвычисляется новое приближение сеточной функции \omega^{(k + 1)}_{ij} = \omega^{(k)}_{ij} - \tau_{k + 1} r^{(k)}_{ij}, где невязка r^{(k)} = A \omega^{(k)} - B, а итерационный параметр

\tau_{(k + 1)} = \frac{[Ar^{(k)}, r^{(k)}]}{||Ar^{(k)}||^2}.

Для проверки условия остановки вычисляется норма разности между новым и старым приближением. За более подробным описанием отправляю читателя к профильной литературе, например, книге А.А. Самарского [1].

Формулы аппроксимации (по просьбам трудящихся)

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

-\Delta _h \omega_{i,j} + q_{i,j} \omega_{i,j} = F_{i,j}, \space i=\overline{1,M - 1}, \space j=\overline{1,N - 1},

где F_{i, j}=F(x_i, y_j), q_{i, j}= q(x_i, y_j),а разностный оператор Лапласа

\Delta \omega_{i, j} = \frac{1}{h_1}(\frac{\omega_{i+1, j} - \omega_{i, j}}{h_1} - \frac{\omega_{i, j} - \omega_{i - 1, j}}{h_1}) + \\  +\frac{1}{h_2}(\frac{\omega_{i, j+1} - \omega_{i, j}}{h_2} - \frac{\omega_{i, j} - \omega_{i, j - 1}}{h_2}).

Это аппроксимация второго порядка точности. Если сделать для граничных точек аналогично (наивно), то получится аппроксимация первого порядка точности (что испортит точность всей схемы), поэтому применяется более хитрая схема. Рассмотрим на примере правой границы:

\frac{\partial u}{\partial x}(x, y) + u(x, y) = \psi(x, y),

С помощью ряда Тейлора выразим производную:

\frac{\partial u}{\partial x}(x_i, y_j) = \frac{u(x_i, y_j) - u(x_{i - 1}, y_j)}{h_1}+\frac{h_1}{2} \frac{\partial^2u}{\partial x^2}(x_i, y_j) + O(h_1^2)

Переходим к сеточной функции, подставляем в граничное условие (вторая производная выражается через исходное уравнение с оператором Лапласа) и домножаем всё выражение на \frac{2}{h_1},получаем:

\frac{2}{h_1} \frac{\omega_{i, j} - \omega_{i - 1,j}}{h_1} + q_{i, j} u_{i, j} - \frac{1}{h_2}(\frac{\omega_{i, j+1} - \omega_{i, j}}{h_2} - \frac{\omega_{i, j} - \omega_{i, j - 1}}{h_2}) + \\ + \frac{2}{h_1}u_{i,j} = F_{i, j}  + \psi_{i,j}

Здесь i = M, j =\overline{1,N - 1} для правой границы. Аналогичные выражения получаются для остальных границ (можно увидеть в явном виде в исходном коде на github).

В результате основные вычисления на каждом шаге представляют из себя несколько запусков пары вложенных циклов с классической характерной структурой: несколько чтений данных из матриц, арифметические операции и запись в выходную матрицу и/или накопление результата в промежуточной переменной. С производительностью исполнения этих циклов мы и будем разбираться. Вот так выглядит один из них:

for (int i = start_i; i <= stop_i; ++i) {
    for (int j = start_j; j <= stop_j; ++j) {
        int idx = i * (run_config->domain_n + 2) + j;
        double wij  = src[idx];
        double wipj = src[(i + 1) * (run_config->domain_n + 2) + j];
        double wimj = src[(i - 1) * (run_config->domain_n + 2) + j];
        double wijp = src[i       * (run_config->domain_n + 2) + j + 1];
        double wijm = src[i       * (run_config->domain_n + 2) + j - 1];
        double x = (run_config->start_i + i - 1) * run_config->h1;
        double y = (run_config->start_j + j - 1) * run_config->h2;
        double laplacian = (wipj + wimj - 2 * wij) * run_config->sqinv_h1 + (wijp + wijm - 2 * wij) * run_config->sqinv_h2;
        dst[idx] = q(x, y) * wij - laplacian - alpha * F(x, y);
    }
}

Стоит отметить, что постановка учебной задачи подразумевает реализацию параллельной MPI+OpenMP программы, а также (при желании) дополнение программы MPI+CUDA реализацией. Необходимость разбивки области вычислений на домены вносит небольшую специфику в организацию программы, которую я решил здесь сохранить, несмотря на то, что статья посвящена только оптимизации последовательных вычислений и не касается вопросов распараллеливания на несколько процессов.

Дедлайн был вчера: самая обычная программа

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

Полная реализация лежит на github [3]. Основные шаги, проделанные в статье, можно отследить по истории коммитов. Например, вот начальная версия.

Для тестовых замеров будем смотреть на время выполнения 1000 шагов итерационного метода на одном ядре процессора на сетке размером 1000x1000. За эффектом от оптимизаций будем следить на трёх платформах:

  1. «домашний компьютер» — Intel Core i7-9700K @ 4.8 ГГц и компилятор icc (Intel C++ Compiler Classic) 2021.5.0, базовые флаги оптимизации «-O3 -no-prec-div -ansi-alias -xHost -restrict»;

  2. «целевой вычислительный кластер» — IBM POWER8 @ 4.0 ГГц и компилятор IBM XL C/C++ 13.1.6, базовые флаги оптимизации «-O5» (именно на таких процессорах требовалось запускать задачу; с удовольствием бы взял более новую систему, но доступа к свежим процессорам IBM у меня нет);

  3. «главный герой» — Эльбрус-8СВ @ 1.55 ГГц и компилятор lcc 1.25.19, базовые флаги оптимизации «-O4 -ffast -ffast-math -mtune=elbrus-8c2».

Итак, для тестов на Эльбрусе мы взяли наиболее производительный на данный момент процессор (инженерные образцы 6 поколения архитектуры не рассматриваем). У данного процессора есть аппаратная поддержка циклов, векторные регистры, возможности программной конвейеризации циклов, механизм подкачки массивов (APB) и вообще 6 «двухэтажных» АЛУ для вычислений с плавающей точкой, а мы взяли достаточно свежий компилятор и хорошие опции оптимизации. Несмотря на то, что для векторизации требуется соблюдение выравнивания данных, мы надеемся получить хороший результат.

Смотрим на результаты запуска и немного огорчаемся: Эльбрус отстаёт в 5-16 раз от Intel и IBM, впрочем, последние тоже не радуют скоростью. Здесь можно отправиться читать Руководство по эффективному программированию на платформе «Эльбрус» [2], а я просто воспользуюсь опытом, полученным при реализации криптографических примитивов.

Реализация

i7-9700K

POWER8

Эльбрус-8СВ

№0 — базовая

14.1 с

42.9 с

223.6 с

И всё-таки линейкой по пальцам: подстановка «горячих» функций компилятором

Об этом просто невозможно промолчать: нельзя вызывать простые функции в циклах из других единиц трансляции. Надеюсь, дорогой читатель никогда так не делает, если хочет производительности от своей программы. Да, речь сейчас о q(x, y) и F(x, y), если мы решили вычислять их явно на каждой итерации. Не рассматривая случай какой-нибудь универсальности, эти функции должны быть определены строго в том же файле, где вызываются, чтобы компилятор имел возможность подставить их (inlining) в тело цикла при обычной компиляции (без Link Time Optimization, межпроцедурного анализа в масштабах всей программы или режима -fwhole на Эльбрусе).

Давайте просто посмотрим, как изменится время выполнения программы, если перенести определения функций q(x, y) и F(x, y) из отдельного файла (куда они могли попасть из соображений удобства) поближе к месту их вызова.

Реализация

i7-9700K

POWER8

Эльбрус-8СВ

№0 — базовая

14.1 с

42.9 с

223.6 с

№1 — возможен inline

4.8 c

26.2 c

102.0 c

Примечательно, что на Эльбрусе, который имеет фиксированные и относительно большие штрафы за вызов функций, ускорение составляет 2.2 раза, а на Intel и IBM — 2.9 и 1.5 раза, соответственно. Так что на предсказатель переходов и внеочередное исполнение надейся, а сам не плошай.

Удар ниже пояса: правильный тип для счётчика цикла

Так вышло, что использовать сейчас в качестве счётчика цикла на Эльбрусе любой тип, существенно отличающийся от long — крайне неудачная идея. Связано это с тем, что компилятору необходимо следовать стандарту языка Си, а также с рядом особенностей в реализации компилятора.

В Руководстве нет подробных комментариев, только сказано, что рекомендуется использовать тип long (знаковый). Наверное, идеологически более правильно было бы рекомендовать ssize_t, но тут надо уточнить наличие определения ssize_t на других платформах. Да и с точки зрения компилятора конкретно на Эльбрусе отличий нет: это всё знаковый 64-битный тип.

Что ж, заменяем все границы и счётчики на ssize_t (определение через my_int для удобства экспериментов) и получаем ускорение в 5.4 раза: на 1000 итераций теперь требуется 18.8 секунд вместо 102. В зависимости от ряда факторов, ускорение от подобной замены может быть как почти незаметным, так и более весомым.

Так чем плохи другие типы? Для успешного применения механизма подкачки массивов APB необходимо гарантировать линейность изменения адресов элементов массива в цикле. К сожалению, это сразу отрезает возможность эффективного использования беззнакового типа: в нём допустимо переполнение, а значит на этапе компиляции невозможно гарантировать условие i + 1 > i. Так что привычный многим беззнаковый size_t отпадает. Хотя конкретно в данном примере size_t немного выигрывает, при дальнейших оптимизациях, как правило, проявляется небольшая просадка времени выполнения на беззнаковом типе. Впрочем, разница не так велика и можно продолжать спокойно использовать size_t.

А у меня всё в int, что я делаю не так? Это пример ситуации, когда формально проблемы нет, но есть некоторые сложности в реализации компилятора. Оптимизации применяются на разных стадиях работы компилятора и не всегда успешно согласуются друг с другом. В нашем случае компилятор на более раннем этапе видит цикл и расширяет счётчик до естественного для процессора 64-битного типа. Затем при попытке применить APB внутри цикла происходит вычисление смещения с использованием другой переменной типа int — run_config->domain_n. Получается несогласованность типов переменных, необходимо преобразование, что и приводит к вопиющему снижению производительности. Интересно, что можно вынести из внутреннего цикла чтение domain_n в локальную int переменную и тоже получить ускорение, так как в этом случае расширение переменной до 64 бит происходит в более ранней фазе компиляции.

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

Наконец, таблица с замерами этого раздела. Хотя на Intel и IBM лучший результат показывает тип int, мы остановимся на ssize_t, чтобы не плодить платформозависимых изменений.

Реализация

i7-9700K

POWER8

Эльбрус-8СВ

№1

4.8 c

26.2 c

102.0 c

№1 + локальные int переменные

4.8 c

26.1 c

24.0 с

№1 + size_t счётчики

5.3 с

29.2 с

18.4 с

№2 — ssize_t счётчики

5.3 с

27.9 с

18.8 с

Галопом по Европам: несколько алгоритмических оптимизаций

Этот раздел посвящён традиционным оптимизациям, которые приходят с опытом и обычно дают положительный эффект.

Во-первых, это предвычисления значений функций q(x, y) и F(x, y). На протяжении всего итерационного процесса они не меняются, так что нет смысла тратить время на лишнюю арифметику, тем более, эти функции на практике могут быть более сложными, чем в рассматриваемом примере. Вообще говоря, может оказаться, что считать эти функции будет быстрее, чем загружать значения из памяти, но я не уверен, что такое поведение сохранится после применения дальнейших оптимизаций.

Во-вторых, это уменьшение количества проходов по массиву. В данном случае очевидно, что при вычислении итерационного параметра можно совместить вычисление скалярного произведения для числителя и нормы для знаменателя. Чуть менее заметно второе место: вычисление текущего значения итерационной ошибки можно делать вместе с обновлением сеточной функции. Заметим, что полагаться на компилятор здесь не получается, так как циклы попадают в разные функции, вызываемые из другой единицы трансляции.

В-третьих, это обмен указателей вместо копирования матрицы для перехода к новой итерации. Копирование матрицы вместо обмена указателей увеличивает время работы программы на всех рассматриваемых платформах на 3-8%.

В сумме указанные оптимизации дают ускорение на всех платформах от нескольких процентов до нескольких разов. Заметно выбивается Intel, на котором проявляется замедление при переходе к предвычислениям функций. Это место неплохо бы происследовать, но мы пока удовлетворимся хорошим ускорением на Эльбрусе и IBM.

Реализация

i7-9700K

POWER8

Эльбрус-8СВ

№2

5.3 с

27.9 с

18.8 с

+ предвычисления q, F

5.98 с

10.72 с

14.23 с

+ меньше проходов по массиву

5.32 с

8.88 с

12.35 с

+ отказ от memcpy — №3

4.93 с

8.42 с

12.03 с

Панки грязи не боятся: читаем на языке ассемблера

Разобраться совсем не так сложно, как может показаться на первый взгляд. Работает универсальный приём: просто берём строчку компиляции нужного файла и заменяем флаг -c на -S, чтобы получить файл с ассемблерным кодом. Не забываем добавить -fverbose-asm, эта опция значительно упрощает чтение на языке ассемблера: для каждой инструкции строится привязка к строке исходного кода, а также включается статический подсчёт тактов (позволяет точно оценить время работы при отсутствии блокировок во время исполнения). Дальше дело за малым: выбираем строчку внутри интересующего нас цикла и находим её в ассемблерном коде по номеру.

Возьмём, например, последний вложенный цикл (из функции update_w_calc_partial_error). Так как компилятор делает ряд оптимизаций по конвейеризации циклов, расщеплению по условиям и создаёт участки компенсирующего кода, то нас прежде всего интересуют вхождения нашей строки в широкие команды (ШК) с меткой loop_mode. Таких оказывается 2, второй выглядит получше, но начнём с первого:

.L53534:						! solve_cpu.c : 303
	! <0073>
	{
	  loop_mode
	  rbranch	.L63957
	  ldd,0,sm	%dr4, %db[3], %db[22], mas=0x4			! solve_cpu.c : 305
	  fmuld,1,sm	%dr1, %db[26], %db[27]				! solve_cpu.c : 305
	  ldd,2	%dr4, %db[17], %db[36], mas=0x3 ? %pcnt0
	  addd,3,sm	0x8, %db[3], %db[1]				! solve_cpu.c : 303
	  fsubd,4,sm	%db[21], %db[33], %db[38]				! solve_cpu.c : 306
	  ldd,5	%dr3, %db[17], %db[25], mas=0x3 ? %pcnt0
	}
.L63965:
	! <0074>
	{
	  loop_mode
	  alc	alcf=1, alct=1			! solve_cpu.c : 303
	  abn	abnf=1, abnt=1			! solve_cpu.c : 303
	  ct	%ctpr1 ? %NOT_LOOP_END				! solve_cpu.c : 303
	  fmuld,1,sm	%dr0, %db[30], %db[39]				! solve_cpu.c : 307
	  ldd,3,sm	%dr3, %db[9], %db[17], mas=0x4 ? %pcnt4			! solve_cpu.c : 306
	  fmul_addd,4,sm	%db[45], %db[37], %db[20], %db[12]			! solve_cpu.c : 307
	  staad,5	%db[42], %aad0[ %aasti1 ]
	  incr,5	%aaincr0	! solve_cpu.c : 306
	}

Видим, что основная часть тела цикла вроде как занимает всего 2 такта, но постойте-ка: инструкция rbranch делает безусловный переход на метку .L63957, где мы видим ещё минимум 11 тактов:

.L63957:
	! <0790>
	{
	  nop 3
	  fmuld,0,sm	%dr1, %db[36], %db[37]				! solve_cpu.c : 305
	  fmuld,1,sm	%dr0, %db[36], %db[45]				! solve_cpu.c : 307
	}
	! <0794>
	{
	  nop 5
	  fsubd,0,sm	%db[25], %db[37], %db[42]				! solve_cpu.c : 306
	}
	! <0800>
	{
	  ibranch	.L63965				! solve_cpu.c : 307
	}

В этом отдельном куске только арифметика с выдерживанием задержек от инструкций, так что вернёмся к анализу первой части. Замечаем, что загрузки из памяти делаются инструкцией ldd с тегами mas=0x4 и mas=0x3. Ага, замечательно, это у нас применился динамический механизм разрыва зависимостей с использованием аппаратной таблицы DAM (Memory Access Disambiguator). Секундочку...

Соблюдаем социальную дистанцию: разрыв зависимостей

При реализации подобных численных методов (да и вообще при решении многих других задач) обычно удобно соблюдать простое правило: участки памяти по разным указателям не пересекаются. В действительности, при написании функций я уже держал эту информацию в голове и полагался на независимость указателей. Только вот почему бы не рассказать об этом компилятору, раз мы обладаем такой информацией?

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

В случае Эльбруса проявляется ещё и другая особенность: из-за статического планирования без информации о независимости итераций мы не можем начать исполнять следующую итерацию до окончания текущей. Это сразу лишает нас важнейшего способа быстрого исполнения циклов в условиях статического планирования — программной конвейеризации цикла.

Так что же делать? Начать можно с простого пути: меньше всего компилятор знает об указателях, которые приходят в качестве параметров функции, поэтому мы можем воспользоваться ключом -frestrict-params, говоря компилятору: «считай, что на всех указателях, являющихся параметрами функций, есть модификатор restrict». Только вот наш случай сложнее: часть указателей для удобства упакована в структуру и их разыменование выполняется в 2 шага (сначала читаем указатель из структуры, потом уже по нему обращаемся к массиву). В результате одного restrict на «внешнем» указателе уже недостаточно. Есть более более мощная комбинация флагов -frestrict-all -frestrict-unsafe, но её всерьёз рассматривать не будем.

В lcc 1.25 впервые появилась поддержка прагмы ivdep, предназначенной для игнорирования возможных зависимостей в цикле. Можно попробовать воспользоваться ею, но я предпочитаю вариант с копированием нужных указателей и явным добавлением модификатора restrict. На мой взгляд, такой вариант самый прозрачный и даже немного упрощает код программы. Вот так, например, будет выглядеть начало одной из 3 подправленных функций:

double update_w_calc_partial_error(const struct RunConfig *run_config, double tau)
{
    double * restrict cur_w = run_config->cur_w;
    double * restrict next_w = run_config->next_w;
    double * restrict residual = run_config->residual;
    // <...>
}

В результате небольшой модификации мы добились ускорения на 33-80% на всех платформах. Подчеркну, что наша оптимизация является общей, а не специфической для Эльбруса. Более того, наиболее выраженный эффект от её применения наблюдается на IBM.

Реализация

i7-9700K

POWER8

Эльбрус-8СВ

№3

4.93 с

8.42 с

12.03 с

№4 — используем restrict

3.69 с

4.69 с

7.48 с

И проверим, как же теперь выглядит исследуемый цикл на языке ассемблера. Отлично, все вхождения цикла стали компактными, раскрученными на 2 и спланированными в 4 такта на тело (то есть 2 такта на итерацию):

Цикл на языке ассемблера
.L38838:						! solve_cpu.c : 312
	! <0237>
	{
	  loop_mode
	  fmuld,5,sm	%dr7, %db[11], %db[15]				! solve_cpu.c : 314
	  movad,0	area=0, ind=0, am=0, be=0, %db[0]	! solve_cpu.c : 315
	  movad,1	area=0, ind=8, am=1, be=0, %db[1]	! solve_cpu.c : 315
	  movad,3	area=0, ind=0, am=0, be=0, %db[8]	! solve_cpu.c : 314
	}
	! <0238>
	{
	  loop_mode
	  fmuld,3,sm	%dr0, %db[11], %db[20]				! solve_cpu.c : 316
	  fmuld,4,sm	%dr7, %db[12], %db[16]				! solve_cpu.c : 314
	  fmuld,5,sm	%dr0, %db[12], %db[21]				! solve_cpu.c : 316
	}
	! <0239>
	{
	  loop_mode
	  fmuld,3,sm	%db[22], %db[17], %db[11]				! solve_cpu.c : 316
	  fmuld,4,sm	%db[23], %db[18], %db[24]				! solve_cpu.c : 316
	  fsubd,5,sm	%db[7], %db[17], %db[12]				! solve_cpu.c : 315
	}
	! <0240>
	{
	  loop_mode
	  alc	alcf=1, alct=1			! solve_cpu.c : 312
	  abn	abnf=1, abnt=1			! solve_cpu.c : 312
	  ct	%ctpr1 ? %NOT_LOOP_END				! solve_cpu.c : 312
	  fsubd,1,sm	%db[6], %db[18], %db[17]				! solve_cpu.c : 315
	  staad,2	%db[19], %aad2[ %aasti1 ]		! solve_cpu.c : 315
	  faddd,3,sm	%dr3, %db[13], %dr3				! solve_cpu.c : 316
	  faddd,4,sm	%dr1, %db[26], %dr1				! solve_cpu.c : 316
	  staad,5	%db[14], %aad2[ %aasti1 + _f32s,_lts0 0x8 ]
	  incr,5	%aaincr2	! solve_cpu.c : 315
	  movad,3	area=0, ind=8, am=1, be=0, %db[7]	! solve_cpu.c : 314
	}

От работы кони дохнут: вынос инварианта из цикла

Обратимся теперь к циклу из функции calc_tau_part:

for (my_int j = 2; j <= run_config->domain_n - 1; ++j) {
    my_int idx = i * (run_config->domain_n + 2) + j;
    num += rhox * a_residual[idx] * residual[idx];
    div += rhox * a_residual[idx] * a_residual[idx];
}

Легко видеть, что здесь есть 2 умножения на величину rhox, значение которой не меняется в цикле. Её можно вынести за пределы цикла, так как мы изначально допускаем нарушение порядка арифметических операций. Конечно, это всего лишь одно умножение, но важна сама идея, так как в других примерах легко можно встретить вычисление в цикле, скажем, синуса от постоянной величины. Примечательно, что в документации к IBM XLC говорится о том, что компилятор умеет сам делать вынос инварианта. Что ж, замеры показывают, что надёжнее это сделать руками:

Реализация

i7-9700K

POWER8

Эльбрус-8СВ

№4

3.69 с

4.69 с

7.48 с

№5 — вынос инварианта

3.67 с

4.37 с

7.08 с

Эффект на Эльбрусе хорошо иллюстрируется: теперь цикл спланирован в 1 такт без раскрутки вместо 4 тактов при раскрутке на 2.

Для любителей ассемблера

Было:

.L19038:						! solve_cpu.c : 270
	! <0259>
	{
	  loop_mode
	  movad,2	area=0, ind=8, am=1, be=0, %db[1]	! solve_cpu.c : 272
	  movad,3	area=0, ind=0, am=0, be=0, %db[0]	! solve_cpu.c : 272
	}
	! <0260>
	{
	  loop_mode
	  fmuld,4,sm	%dr5, %db[3], %db[15]				! solve_cpu.c : 272
	  fmuld,5,sm	%dr5, %db[2], %db[12]				! solve_cpu.c : 272
	  movad,0	area=0, ind=8, am=1, be=0, %db[6]	! solve_cpu.c : 272
	  movad,1	area=0, ind=0, am=0, be=0, %db[9]	! solve_cpu.c : 272
	}
	! <0261>
	{
	  loop_mode
	  fmuld,3,sm	%db[17], %db[10], %db[19]				! solve_cpu.c : 272
	  fmuld,4,sm	%db[17], %db[5], %db[16]				! solve_cpu.c : 273
	  fmuld,5,sm	%db[14], %db[13], %db[20]				! solve_cpu.c : 272
	}
	! <0262>
	{
	  loop_mode
	  alc	alcf=1, alct=1			! solve_cpu.c : 270
	  abn	abnf=1, abnt=1			! solve_cpu.c : 270
	  ct	%ctpr1 ? %NOT_LOOP_END				! solve_cpu.c : 270
	  fmuld,1,sm	%db[14], %db[4], %db[5]				! solve_cpu.c : 273
	  faddd,2,sm	%dr0, %db[7], %dr0				! solve_cpu.c : 273
	  faddd,3,sm	%dr1, %db[21], %dr1				! solve_cpu.c : 272
	  faddd,4,sm	%dr3, %db[18], %dr3				! solve_cpu.c : 273
	  faddd,5,sm	%dr2, %db[22], %dr2				! solve_cpu.c : 272
	}

Стало:

.L18685:						! solve_cpu.c : 272
	! <0083>
	{
	  loop_mode
	  alc	alcf=1, alct=1			! solve_cpu.c : 272
	  abn	abnf=1, abnt=1			! solve_cpu.c : 272
	  ct	%ctpr1 ? %NOT_LOOP_END				! solve_cpu.c : 272
	  fmuld,1,sm	%db[37], %db[36], %db[39]				! solve_cpu.c : 274
	  faddd,2,sm	%db[25], %db[47], %db[17]				! solve_cpu.c : 274
	  fmuld,4,sm	%db[37], %db[37], %db[38]				! solve_cpu.c : 275
	  faddd,5,sm	%db[24], %db[46], %db[16]				! solve_cpu.c : 275
	  movad,1	area=0, ind=0, am=1, be=0, %db[26]	! solve_cpu.c : 274
	  movad,3	area=0, ind=0, am=1, be=0, %db[27]	! solve_cpu.c : 274
	}

Ну, кошечка, ну ещё капельку: добиваемся векторизации

Существенное ограничение на Эльбрус-8СВ при работы с векторными регистрами — данные должны быть выровнены (по 16-байтной границе). Без этого компилятор не сможет сгенерировать код с использованием 128-битных регистров. А вот как получить векторный код при соблюдении необходимых требований — «а вот это науке ещё не известно!» Точнее, рецепт примерно стандартный: в текущей версии компилятору нужна информация о количестве итераций цикла, чтобы «решиться» на векторизацию. Эта информация может быть получена из подсказки с помощью прагмы loop count(N) или из профиля исполнения программы. Тем не менее, я пока не выработал для себя удобного способа работы, так что покажу способ для продвинутых: как с помощью intrinsic-функций гарантированно получить векторный код.

Основные усилия мы направим на цикл в функции calc_aw_b. Его особенность в том, что на каждой итерации нужны 3 подряд идущих элемента массива. Соответственно, выровненный доступ к памяти здесь просто так невозможен. Для обхода этой особенности на итерации с номером j будем формировать регистр, содержащий сдвинутое на 1 значение (как бы src[j + 1] элемент), с помощью ассемблерной инструкции qppermb из считанных выровненных 128-битных значений src[j] и src[j + 2]. То есть из src[j] мы берём старшую половину, а из src[j + 2] — младшую.

В остальном достаточно заменить обычные арифметические операции на вызов соответствующих intrinsic-функций и получится цикл такого вида:

for (my_int j = start_j; j < stop_j; j += 2) {
    my_int idx = i * (run_config->domain_n + 2) + j;
    __v2di wij = *((__v2di *) &src[idx]);
    __v2di wipj = *((__v2di *) &src[(i + 1) * (run_config->domain_n + 2) + j]);
    __v2di wimj = *((__v2di *) &src[(i - 1) * (run_config->domain_n + 2) + j]);
    
    src_0 = wij;
    src_1 = *((__v2di *) &src[idx + 2]);
    __v2di wijp = __builtin_e2k_qppermb(src_1, src_0, mix_doubles);
    
    __v2di t0 = __builtin_e2k_qpfaddd(wipj, wimj);
    __v2di t1 = __builtin_e2k_qpfmuld(const_2, wij);
    __v2di t2 = __builtin_e2k_qpfaddd(wijp, wijm);
    __v2di t3 = __builtin_e2k_qpfsubd(t0, t1);
    __v2di t4 = __builtin_e2k_qpfsubd(t2, t1);
    __v2di t5 = __builtin_e2k_qpfmuld(t3, sqinv_h1);
    __v2di t6 = __builtin_e2k_qpfmuld(t4, sqinv_h2);
    __v2di laplacian = __builtin_e2k_qpfaddd(t5, t6);
    __v2di t7 = __builtin_e2k_qpfmuld(wij, *((__v2di *) &q_mat[idx]));
    __v2di t8 = __builtin_e2k_qpfmuld(v2alpha, *((__v2di *) &b_mat[idx]));
    *((__v2di *) &dst[idx]) = __builtin_e2k_qpfsubd(__builtin_e2k_qpfsubd(t7, laplacian), t8);
    wijm = wijp;
}

Выглядит уже довольно громоздко, а это ещё не видна обработка головы и хвоста цикла. Зато компилируется в красивую конструкцию с итоговым темпом обработки 1 такт на элемент матрицы.

.L15436:						! solve_cpu.c : 234
	! <0247>
	{
	  loop_mode
	  qpfadd_rsubd,0,sm	%xb[38], %xb[40], %xb[95], %xb[40]			! solve_cpu.c : 248
	  qpfadd_rsubd,1,sm	%xb[82], %xb[92], %xb[97], %xb[1]			! solve_cpu.c : 247
	  qpfmuld,2,sm	%xb[48], %xr5, %xb[56]				! solve_cpu.c : 250
	  qpfmul_addd,4,sm	%xb[13], %xr6, %xb[62], %xb[48]			! solve_cpu.c : 251
	  qpfmul_rsubd,5,sm	%xb[32], %xb[89], %xb[56], %xb[13]			! solve_cpu.c : 254
	  movaqp,1	area=1, ind=0, am=1, be=0, %xb[0]	! solve_cpu.c : 242
	}
	! <0248>
	{
	  loop_mode
	  alc	alcf=1, alct=1			! solve_cpu.c : 234
	  abn	abnf=1, abnt=1			! solve_cpu.c : 234
	  ct	%ctpr1 ? %NOT_LOOP_END				! solve_cpu.c : 234
	  qppermb,1,sm	%xb[6], %xb[8], %xr0, %xb[36] ? %pcnt21			! solve_cpu.c : 242
	  qpfmuld,2,sm	%xr2, %xb[4], %xb[89]				! solve_cpu.c : 245
	  qpfmul_subd,4,sm	%xr7, %xb[59], %xb[21], %xb[62]			! solve_cpu.c : 254
	  staaqp,5	%xb[72], %aad5[ %aasti1 ]
	  incr,5	%aaincr0	! solve_cpu.c : 254
	  movaqp,0	area=0, ind=0, am=1, be=0, %xb[21]	! solve_cpu.c : 253
	  movaqp,1	area=2, ind=0, am=1, be=0, %xb[72]	! solve_cpu.c : 237
	  movaqp,2	area=0, ind=0, am=1, be=0, %xb[59]	! solve_cpu.c : 252
	  movaqp,3	area=1, ind=0, am=1, be=0, %xb[82]	! solve_cpu.c : 238
	}

Здесь хорошо видно, что компилятор успешно объединяет последовательные инструкции в сдвоенные — это позволяет более плотно планировать их исполнение (без «второго этажа» 6 АЛУ не хватило бы, чтобы спланировать цикл в 2 такта на тело). И работает немного быстрее: удаётся улучшить результат на 14% и достичь 6.19 секунд (в таблицах этот шаг будет обозначен как «№6 — ручная векторизация»). Заметим, что на рассматриваемых процессорах Intel и IBM нет требований к выравниванию данных, а потому компиляторы ещё на предыдущих этапах смогли справиться с векторизацией всех циклов.

Ломаем законы физики: линейная обработка против объёма вычислений

Продолжая тему неочевидных оптимизаций, можно вспомнить, что векторизация циклов даётся не бесплатно. Для Эльбруса мы сделали явную обработку начала и конца цикла из-за выравнивания. В случае двух других архитектур ситуация немного проще, но обработку хвоста цикла никто не отменял, хоть ответственность за генерацию соответствующих инструкций и легла на компилятор. На Эльбрусе есть ещё один нюанс: подготовка APB занимает некоторое количество тактов перед циклом.

Так зачем мы всё это делаем во внешнем цикле, если по факту двумерный массив представлен в памяти линейным участком? Заменяем двойной цикл (с количеством итераций не больше M для внешнего и N для вложенного) на одинарный с общим количеством итераций MN + 2M - 2 и поднимаем его в начало функции. Граничные точки будут перезаписаны ниже при обработке границ. Да, мы в этом цикле делаем больше вычислений, чем было, и даже что-то считаем для точек, которые нам вообще не нужны (используются при межпроцессных синхронизациях), но избавление от вложенного цикла даёт выигрыш. Напрашивается аналогия с работой кэша: при переходе к линейному доступу к памяти можно делать больше операций, но выигрыш всё равно останется.

После перехода к одному большому циклу на Эльбрусе можно проверить поведение программы при выборе параметра unroll для этого цикла. Обычно требуется посмотреть всего несколько значений (до 4 или 8), в данном случае оптимальным оказалась раскрутка на 3 итерации. На Intel и IBM пользы от ручного выбора параметра раскрутки не обнаружилось.

Реализация

i7-9700K

POWER8

Эльбрус-8СВ

№6 — ручная векторизация

3.67 с

4.37 с

6.19 с

№7 — коллапсирование гнезда циклов

3.60 с

4.25 с

5.46 с

Последний штрих: процессор быстрее памяти

Остался один очевидный шаг, который можно было бы сделать ещё на этапе алгоритмических оптимизаций, но я сознательно пропустил его в угоду унификации. В функции calc_aw_b параметр alpha принимает значения 0 или 1 и отвечает за необходимость вычитания правой части при вычислении AX - \alpha B. Эти 2 случая можно было бы сразу разделить, но я предлагаю взглянуть на проблему под другим углом.

Основные циклы на языке ассемблера сейчас выглядят так:
.L13023:						! solve_cpu.c : 168
	! <0052>
	{
	  loop_mode
	  qpfadd_rsubd,1,sm	%xb[61], %xb[36], %xb[95], %xb[15]			! solve_cpu.c : 180
	  qpfmsd,3,sm	%xb[15], %xb[87], %xb[91], %xb[103]			! solve_cpu.c : 187
	  qpfnmad,4,sm	%xr11, %xb[50], %xb[108], %xb[104]			! solve_cpu.c : 187
	  qpfmuld,5,sm	%xb[65], %xr0, %xb[105]				! solve_cpu.c : 183
	}
	! <0053>
	{
	  loop_mode
	  qpfadd_rsubd,1,sm	%xb[110], %xb[6], %xb[68], %xb[29]			! solve_cpu.c : 181
	  qpfmuld,2,sm	%xr10, %xb[35], %xb[16]				! solve_cpu.c : 178
	  qpfmsd,3,sm	%xb[43], %xb[29], %xb[109], %xb[106]			! solve_cpu.c : 187
	  qppermb,4,sm	%xb[5], %xb[35], %xr12, %xb[4] ? %pcnt5			! solve_cpu.c : 175
	}
	! <0054>
	{
	  loop_mode
	  qpfadd_rsubd,1,sm	%xb[67], %xb[110], %xb[93], %xb[61]			! solve_cpu.c : 181
	  qpfmuld,2,sm	%xr10, %xb[5], %xb[66]				! solve_cpu.c : 178
	  qpfmad,3,sm	%xb[66], %xr9, %xb[111], %xb[107]			! solve_cpu.c : 184
	  qppermb,4,sm	%xb[33], %xb[90], %xr12, %xb[65]			! solve_cpu.c : 175
	  movaqp,0	area=0, ind=0, am=0, be=0, %xb[43]	! solve_cpu.c : 186
	  movaqp,1	area=0, ind=16, am=1, be=0, %xb[50]	! solve_cpu.c : 186
	  movaqp,3	area=2, ind=0, am=1, be=0, %xb[36]	! solve_cpu.c : 186
	}
	! <0055>
	{
	  loop_mode
	  qpfmuld,2,sm	%xb[31], %xr0, %xg16				! solve_cpu.c : 183
	  qpfmad,3,sm	%xb[76], %xr9, %xg16, %xb[87]			! solve_cpu.c : 184
	  qppermb,4,sm	%xb[90], %xb[5], %xr12, %xb[108]			! solve_cpu.c : 175
	  qpfmuld,5,sm	%xb[75], %xr0, %xb[109]				! solve_cpu.c : 183
	  movaqp,0	area=1, ind=0, am=0, be=0, %xb[88]	! solve_cpu.c : 175
	  movaqp,1	area=1, ind=16, am=1, be=0, %xb[31]	! solve_cpu.c : 175
	  movaqp,2	area=0, ind=0, am=0, be=0, %xb[75]	! solve_cpu.c : 185
	  movaqp,3	area=0, ind=16, am=1, be=0, %xb[76]	! solve_cpu.c : 185
	}
	! <0056>
	{
	  loop_mode
	  qpfadd_rsubd,1,sm	%xb[30], %xb[24], %xb[18], %xb[62]			! solve_cpu.c : 180
	  qpfmuld,2,sm	%xr10, %xb[90], %xb[91]				! solve_cpu.c : 178
	  qpfmad,3,sm	%xb[17], %xr9, %xb[105], %xb[95]			! solve_cpu.c : 184
	  qpfnmad,4,sm	%xr11, %xb[62], %xb[100], %xb[99]			! solve_cpu.c : 187
	  staaqp,5	%xb[102], %aad5[ %aasti1 ]		! solve_cpu.c : 187
	  movaqp,0	area=3, ind=0, am=1, be=0, %xb[17]	! solve_cpu.c : 185
	  movaqp,1	area=4, ind=0, am=1, be=0, %xb[18]	! solve_cpu.c : 171
	  movaqp,2	area=4, ind=0, am=1, be=0, %xb[24]	! solve_cpu.c : 170
	  movaqp,3	area=1, ind=16, am=0, be=0, %xb[30]	! solve_cpu.c : 171
	}
	! <0057>
	{
	  loop_mode
	  alc	alcf=1, alct=1			! solve_cpu.c : 168
	  abn	abnf=1, abnt=1			! solve_cpu.c : 168
	  ct	%ctpr1 ? %NOT_LOOP_END				! solve_cpu.c : 168
	  qpfadd_rsubd,0,sm	%xb[72], %xb[71], %xb[68], %xb[72]			! solve_cpu.c : 180
	  qpfadd_rsubd,1,sm	%xb[4], %xb[67], %xb[16], %xb[71]			! solve_cpu.c : 181
	  staaqp,2	%xb[101], %aad5[ %aasti1 + _f32s,_lts1 0x10 ]	! solve_cpu.c : 187
	  qpfmsd,3,sm	%xb[98], %xb[86], %xb[97], %xb[98]			! solve_cpu.c : 187
	  qpfnmad,4,sm	%xr11, %xb[55], %xb[103], %xb[100]			! solve_cpu.c : 187
	  staaqp,5	%xb[104], %aad5[ %aasti1 + _f32s,_lts0 0x20 ]
	  incr,5	%aaincr2	! solve_cpu.c : 187
	  movaqp,0	area=2, ind=16, am=1, be=0, %xb[55]	! solve_cpu.c : 170
	  movaqp,1	area=2, ind=0, am=0, be=0, %xb[68]	! solve_cpu.c : 170
	  movaqp,2	area=1, ind=0, am=1, be=0, %xb[67]	! solve_cpu.c : 171
	  movaqp,3	area=3, ind=0, am=1, be=0, %xb[1]	! solve_cpu.c : 175
	}


.L18027:						! solve_cpu.c : 321
	! <0064>
	{
	  loop_mode
	}
	! <0065>
	{
	  loop_mode
	  movaqp,1	area=0, ind=16, am=0, be=0, %xb[1]	! solve_cpu.c : 324
	  movaqp,2	area=0, ind=0, am=0, be=0, %xb[0]	! solve_cpu.c : 323
	  movaqp,3	area=0, ind=16, am=1, be=0, %xb[4]	! solve_cpu.c : 323
	}
	! <0066>
	{
	  loop_mode
	  qpfmuld,3,sm	%xb[2], %xb[2], %xb[12]				! solve_cpu.c : 326
	  qpfmuld,4,sm	%xb[6], %xb[3], %xb[11]				! solve_cpu.c : 325
	  qpfmuld,5,sm	%xb[6], %xb[6], %xb[8]				! solve_cpu.c : 326
	  movaqp,1	area=0, ind=0, am=1, be=0, %xb[7]	! solve_cpu.c : 324
	}
	! <0067>
	{
	  loop_mode
	  alc	alcf=1, alct=1			! solve_cpu.c : 321
	  abn	abnf=1, abnt=1			! solve_cpu.c : 321
	  ct	%ctpr1 ? %NOT_LOOP_END				! solve_cpu.c : 321
	  qpfmuld,1,sm	%xb[2], %xb[9], %xb[3]				! solve_cpu.c : 325
	  qpfaddd,2,sm	%xr0, %xb[5], %xr0				! solve_cpu.c : 325
	  qpfaddd,3,sm	%xr2, %xb[14], %xr2				! solve_cpu.c : 326
	  qpfaddd,4,sm	%xr3, %xb[13], %xr3				! solve_cpu.c : 325
	  qpfaddd,5,sm	%xr1, %xb[10], %xr1				! solve_cpu.c : 326
	}


.L33648:						! solve_cpu.c : 391
	! <0067>
	{
	  loop_mode
	  movaqp,2	area=0, ind=0, am=0, be=0, %xb[1]	! solve_cpu.c : 393
	  movaqp,3	area=0, ind=16, am=1, be=0, %xb[0]	! solve_cpu.c : 393
	}
	! <0068>
	{
	  loop_mode
	  qpfmuld,4,sm	%xr5, %xb[3], %xb[15]				! solve_cpu.c : 393
	  qpfmuld,5,sm	%xr5, %xb[2], %xb[12]				! solve_cpu.c : 393
	  movaqp,0	area=0, ind=16, am=1, be=0, %xb[6]	! solve_cpu.c : 394
	  movaqp,1	area=0, ind=0, am=0, be=0, %xb[7]	! solve_cpu.c : 394
	}
	! <0069>
	{
	  loop_mode
	  qpfmuld,3,sm	%xb[17], %xb[17], %xb[3]				! solve_cpu.c : 395
	  qpfmuld,4,sm	%xb[14], %xb[14], %xb[2]				! solve_cpu.c : 395
	  qpfsubd,5,sm	%xb[11], %xb[17], %xb[16]				! solve_cpu.c : 394
	}
	! <0070>
	{
	  loop_mode
	  alc	alcf=1, alct=1			! solve_cpu.c : 391
	  abn	abnf=1, abnt=1			! solve_cpu.c : 391
	  ct	%ctpr1 ? %NOT_LOOP_END				! solve_cpu.c : 391
	  qpfsubd,1,sm	%xb[10], %xb[14], %xb[11]				! solve_cpu.c : 394
	  staaqp,2	%xb[13], %aad2[ %aasti1 + _f32s,_lts0 0x10 ]	! solve_cpu.c : 394
	  qpfaddd,3,sm	%xr3, %xb[5], %xr3				! solve_cpu.c : 395
	  qpfaddd,4,sm	%xr4, %xb[4], %xr4				! solve_cpu.c : 395
	  staaqp,5	%xb[18], %aad2[ %aasti1 ]
	  incr,5	%aaincr2	! solve_cpu.c : 394
	}

Видно, что каждый цикл спланирован так, что на обработку одного элемента матрицы в каждом цикле должен тратиться 1 такт. Если же посмотреть на результаты профилировки с помощью утилиты perf, то оказывается, что цикл в функции calc_aw_b исполняется примерно в 1.5 раза дольше, чем другие 2 (функция calc_aw_b вызывается в 2 раза чаще, чем остальные):

Samples: 7K of event 'task-clock:u', Event count (approx.): 7103000000
  Children      Self  Command     Shared Object           Symbol
+   59.75%    59.75%  prog_e2kv5  prog_e2kv5              [.] calc_aw_b
+   20.37%    20.37%  prog_e2kv5  prog_e2kv5              [.] update_w_calc_partial_error
+   18.16%    18.16%  prog_e2kv5  prog_e2kv5              [.] calc_tau_part

Этот эффект объясняется тем, что данные не успевают подгружаться из оперативной памяти с достаточным для процессора темпом. Для эксперимента разделим случаи умножения на 1 и на 0 и в случае умножения на 0 будем не загружать значение матрицы B, а брать какое-нибудь из уже использованных. Планирование цикла принципиально не поменялось, это те же 6 тактов, хоть в нём и на 3 инструкции movaqp меньше.

Аналогичное планирование:
.L13226:						! solve_cpu.c : 194
	! <0053>
	{
	  loop_mode
	  qpfadd_rsubd,1,sm	%xb[69], %xb[79], %xb[48], %xb[28]			! solve_cpu.c : 206
	  qpfmsd,3,sm	%xb[15], %xb[28], %xb[94], %xb[94]			! solve_cpu.c : 213
	  qpfnmad,4,sm	%xr12, %xb[14], %xb[101], %xb[97]			! solve_cpu.c : 213
	  qpfmuld,5,sm	%xb[90], %xr0, %xb[98]				! solve_cpu.c : 209
	  movaqp,0	area=2, ind=0, am=1, be=0, %xb[15]	! solve_cpu.c : 211
	  movaqp,1	area=0, ind=0, am=0, be=0, %xb[16]	! solve_cpu.c : 211
	}
	! <0054>
	{
	  loop_mode
	  qpfadd_rsubd,1,sm	%xb[76], %xb[6], %xb[93], %xb[72]			! solve_cpu.c : 207
	  qpfmuld,2,sm	%xr9, %xb[45], %xb[69]				! solve_cpu.c : 204
	  qpfmsd,3,sm	%xb[53], %xb[27], %xb[102], %xb[99]			! solve_cpu.c : 213
	  qppermb,4,sm	%xb[5], %xb[45], %xr11, %xb[4] ? %pcnt5			! solve_cpu.c : 201
	  movaqp,0	area=0, ind=16, am=1, be=0, %xb[27]	! solve_cpu.c : 211
	  movaqp,1	area=3, ind=0, am=1, be=0, %xb[48]	! solve_cpu.c : 197
	  movaqp,2	area=3, ind=0, am=1, be=0, %xb[53]	! solve_cpu.c : 196
	  movaqp,3	area=1, ind=16, am=0, be=0, %xb[63]	! solve_cpu.c : 196
	}
	! <0055>
	{
	  loop_mode
	  qpfadd_rsubd,1,sm	%xb[36], %xb[76], %xb[46], %xb[86]			! solve_cpu.c : 207
	  qpfmuld,2,sm	%xr9, %xb[5], %xb[91]				! solve_cpu.c : 204
	  qpfmad,3,sm	%xb[34], %xr10, %xb[103], %xb[100]			! solve_cpu.c : 210
	  qppermb,4,sm	%xb[43], %xb[62], %xr11, %xb[34]			! solve_cpu.c : 201
	  movaqp,0	area=1, ind=16, am=1, be=0, %xb[73]	! solve_cpu.c : 197
	  movaqp,1	area=1, ind=0, am=0, be=0, %xb[79]	! solve_cpu.c : 197
	  movaqp,2	area=1, ind=0, am=1, be=0, %xb[85]	! solve_cpu.c : 196
	  movaqp,3	area=2, ind=0, am=1, be=0, %xb[1]	! solve_cpu.c : 201
	}
	! <0056>
	{
	  loop_mode
	  qpfmuld,2,sm	%xb[74], %xr0, %xg16				! solve_cpu.c : 209
	  qpfmad,3,sm	%xb[60], %xr10, %xg16, %xb[90]			! solve_cpu.c : 210
	  qppermb,4,sm	%xb[62], %xb[5], %xr11, %xb[74]			! solve_cpu.c : 201
	  qpfmuld,5,sm	%xb[41], %xr0, %xb[101]				! solve_cpu.c : 209
	  movaqp,2	area=0, ind=0, am=0, be=0, %xb[60]	! solve_cpu.c : 201
	  movaqp,3	area=0, ind=16, am=1, be=0, %xb[41]	! solve_cpu.c : 201
	}
	! <0057>
	{
	  loop_mode
	  qpfadd_rsubd,1,sm	%xb[59], %xb[54], %xb[71], %xb[30]			! solve_cpu.c : 206
	  qpfmuld,2,sm	%xr9, %xb[62], %xb[44]				! solve_cpu.c : 204
	  qpfmad,3,sm	%xb[30], %xr10, %xb[98], %xb[54]			! solve_cpu.c : 210
	  qpfnmad,4,sm	%xr12, %xb[44], %xb[95], %xb[59]			! solve_cpu.c : 213
	  staaqp,5	%xb[96], %aad4[ %aasti1 ]		! solve_cpu.c : 213
	}
	! <0058>
	{
	  loop_mode
	  alc	alcf=1, alct=1			! solve_cpu.c : 194
	  abn	abnf=1, abnt=1			! solve_cpu.c : 194
	  ct	%ctpr1 ? %NOT_LOOP_END				! solve_cpu.c : 194
	  qpfadd_rsubd,0,sm	%xb[89], %xb[83], %xb[93], %xb[56]			! solve_cpu.c : 206
	  qpfadd_rsubd,1,sm	%xb[4], %xb[36], %xb[69], %xb[37]			! solve_cpu.c : 207
	  staaqp,2	%xb[61], %aad4[ %aasti1 + _f32s,_lts1 0x10 ]	! solve_cpu.c : 213
	  qpfmsd,3,sm	%xb[70], %xb[37], %xb[56], %xb[93]			! solve_cpu.c : 213
	  qpfnmad,4,sm	%xr12, %xb[84], %xb[94], %xb[94]			! solve_cpu.c : 213
	  staaqp,5	%xb[97], %aad4[ %aasti1 + _f32s,_lts0 0x20 ]
	  incr,5	%aaincr2	! solve_cpu.c : 213
	}

А вот скорость вычислений выросла на 8%, что подтверждает наличие зависимости времени вычисления от скорости работы памяти. Остаётся честно избавиться от умножения на alpha в обоих случаях и получить итоговый результат. Ниже сводная таблица со всеми основными результатами.

Реализация

i7-9700K

POWER8

Эльбрус-8СВ

№0 — базовая

14.1 с

42.9 с

223.6 с

№1 — возможен inline

4.8 c

26.2 c

102.0 c

№2 — ssize_t счётчики

5.3 с

27.9 с

18.8 с

№3 — алгоритмические оптимизации

4.93 с

8.42 с

12.03 с

№4 — используем restrict

3.69 с

4.69 с

7.48 с

№5 — вынос инварианта

3.67 с

4.37 с

7.08 с

№6 — ручная векторизация

3.67 с

4.37 с

6.19 с

№7 — коллапсирование гнезда циклов

3.60 с

4.25 с

5.46 с

№8 — уменьшаем нагрузку на память

3.43 с

3.80 с

5.04 с

А что там у -fwhole: компиляция в режиме «вся программа»

Иногда задумываешься: а нужно ли столько трудов по оптимизации? Может быть, есть волшебный ключик, при использовании которого компилятор сам выполнит все оптимизации? На роль такой опции лучше всего претендует межпроцедурный анализ в контексте целиковой программы на этапе линковки и, при возможности, профилировка. В случае xlc достаточно продублировать -O5 во флагах линковщика, для icc и lcc надо подать -ipo и -fwhole, соответственно, при компиляции и линковке. А вот результаты:

Реализация

i7-9700K

POWER8

Эльбрус-8СВ

№0 — базовая

5.0 с

19.6 с

94.5 с

№1 — возможен inline

5.0 c

19.8 c

95.1 c

№2 — ssize_t счётчики

5.0 с

19.8 с

18.7 с

№3 — алгоритмические оптимизации

4.90 с

7.05 с

11.01 с

№4 — используем restrict

3.70 с

4.91 с

7.41 с

№5 — вынос инварианта

3.68 с

4.64 с

7.03 с

№6 — ручная векторизация

3.68 с

4.64 с

6.33 с

№7 — коллапсирование гнезда циклов

3.65 с

3.36 с

5.44 с

№8 — уменьшаем нагрузку на память

3.43 с

3.33 с

5.04 с

В целом, всё видно из таблицы, но я подчеркну: межпроцедурный анализ даёт хорошие результаты, но не заменяет оптимизации, которые может применить разработчик.

Заключение

На примере относительно простой учебной программы мне удалось обнаружить основные сложности и продемонстрировать техники, с которыми приходится сталкиваться при оптимизации под Эльбрус. Конечно, есть ещё ряд особенностей, которые не вошли в эту статью, но их вклад я считаю менее заметным и важным на первых порах оптимизации.

С помощью нехитрых приёмов мне удалось получить на Эльбрусе впечатляющее ускорение в 44 раза и приблизить время вычисления на Эльбрусе к результату на современном Intel и IBM POWER8, причём с помощью тех же оптимизаций результат на Intel был улучшен в 4.1 раза, а на IBM — в 11.5 раза (13 раз с учётом межпроцедурного анализа).

Нельзя не отметить, что оптимизация под Эльбрус не отличается принципиально от оптимизации под другие процессоры общего назначения и следует, в основном, тем же принципам. При этом совершенно не надо бояться VLIW-ассемблера: при наличии минимальных навыков работы с любым компилятором не составляет труда разобраться в сгенерированном коде и понять, из-за чего результат не соответствует ожиданиям.

Ссылки

[1] Самарский А.А., Андреев В.Б. Разностные методы для эллиптических уравнений. М. Изд. "Наука". 1976.

[2] Нейман-заде М. И., Королёв С. Д. Руководство по эффективному программированию на платформе "Эльбрус".

[3] Репозиторий на github.

Комментарии (88)


  1. Ostic
    24.01.2022 11:11
    +11

    Впечатляет. Особенно"Эльбрус" - на 2 порядка
    Впечатляет. Особенно"Эльбрус" - на 2 порядка


    1. ZlobniyShurik
      24.01.2022 11:40
      +20

      И пугает... Хоть что-то где-то не учёл и Эльбрус тебя штрафует так, что мало не покажется. Интел, так сказать, более дуракоустойчив.


      1. napa3um
        24.01.2022 12:35
        +13

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


        1. speshuric
          24.01.2022 12:46
          +4

          Да ладно бы была возможность "бороться с регрессом", а если это "просто jit обновился"? Причём JDK/NetCore jit ни фига не такой ювелирный, как в C/C++ - у него просто времени и памяти нет на "вдумчивые оптимизации".


          1. napa3um
            24.01.2022 12:57
            +11

            Да, выглядит очень хрупкой вся эта возня, не для "живой" разработки платформа, а для "потратил год на оптимизацию и закатал весь софт в бетон, чтобы ничего не изменилось". Кажется, всё это должно быть спрятано от прикладного программиста под капотом. Возможно, компилятор все эти оптимизации должен уметь сам проделывать, пусть даже через нейронки и отбор различных вариантов через тесты, но чтобы сам (да, пусть увеличив время компиляции с 10 минут до пары часов :)).

            Перекладывание на плечи программистов этих оптимизационных проблем, кажется, путь в никуда, чисто экономически. Похожим образом (из-за своей сложности для разработчиков) умерла Sega Saturn, помнится, хотя теоретически была мощнее конкурентов. Программисты не любят, когда им на ровном месте подводные камни залетают в спицы (а для сложения двух чисел нужно знать 14 видов возможных ситуаций, чтобы не сломать поток вычислений) :).


            1. amarao
              24.01.2022 14:52
              +12

              Подозрительно напоминает успех Итаника.


        1. Ostic
          24.01.2022 13:11
          +4

          Вряд ли уж так совсем. Оптимизировали какой-то сервис, алгоритм под конкретную машину и пользуйтесь потом. Вряд ли Вам придется постоянно этим заниматься. Самое главное, что данная оптимизация универсальна(!), те оптимизация происходит для всего оборудования, а не так, что типа Элика оптимизировали Интел регрессировал. Поэтому, такие люди, как автор, всегда будут в цене - оптимизация и оптимизация, хоть там, хоть этам.


          1. napa3um
            24.01.2022 13:14
            +3

            В том и дело, что такие программисты, как автор, будут в цене - и в дефиците :). А неизменяемые сервисы ("забетонированная" кодовая база) не так уж востребованы на конкурентном рынке, увы. Ценность современных архитектур - именно в их подвижности, расширяемости.


            1. Ostic
              24.01.2022 13:19

              Вроде и так и не так.

              1. "забетонированная база" оптимизирована для всех рассматриваемых случаев. Если менять железо, то... не так и часто - можно "полирнуть".

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


              1. napa3um
                24.01.2022 13:21
                +2

                Вы в своём сценарии забыли процесс добавления прикладных фич для пользователя, кажется :).


                1. 0x4eadac43
                  25.01.2022 15:10
                  +1

                  Такая работа по оптимизации не для массового продукта, когда условия задачи меняются постоянно. Скорее для каких-то научных расчетов или, как бы назвать, инфраструктурных вещей (криптография, протоколы).


                  1. napa3um
                    25.01.2022 17:07
                    +1

                    Но позиционируется процессор для массового применения, для стандартных прикладных задач, а не для научных вещей. Насколько я понимаю. Или пускай обычные ненаучные пользователи страдают от тормозящего ПО? Ну, это тоже вариант, согласен :).


              1. qw1
                26.01.2022 11:28
                +1

                Если менять железо, то… не так и часто — можно «полирнуть».
                Тут алгоритм на 200 строчек, и автор его вылизывал целый день или больше. А что будет, если дать ему исходники браузера, это ж в голове не поместится. После таких оптимизаций любое внесение изменений подразумевает откат к начальной версии, и всё начинай заново.


    1. AAbrosov
      24.01.2022 13:00
      -1

      Давайте уточним что "на два порядка"="в сто раз". (если мы говорим о десятичных порядках)


      1. Ostic
        24.01.2022 13:02
        +5

        Давайте уточним. Порядки: единицы, десятки, сотни... От сотен до единиц 2 порядка.


        1. omican
          26.01.2022 17:53

          Я для себя обычно принимаю так: на 1 порядок это в 5-30 раз, а все последующие добавляют множитель 10: например, на 3 порядка это в 500-3000 раз.


      1. RomanArzumanyan
        24.01.2022 13:02

        С другой стороны, было три разряда слева от разделителя, стал один.

        На два разряда стало быстрее.


        1. Ostic
          24.01.2022 13:22

          ну да, когда говорят, например, что сложность логарифмическая, то не считают же 2log(n) или 3log(n), если это не n*log(n) )


  1. Andy_U
    24.01.2022 12:17

    А не могли бы Вы привести еще и количество шагов по X и Y? Хочется оценить количество неизвестных. Просто если их немного, то систему линейных уравнений можно решить в "лоб". Если много, воспользоваться каким-нибудь методом Ньютона-Крылова, и это в обоих случаях будет заметно быстрее рассматриваемого способа. Да еще воспользоваться готовой MKL библиотекой. А вот как поведут себя всякие BLAS'ы и lapack'и на Эльбрусе? Можно ли будет их так же заметно ускорить?


    1. shcher Автор
      24.01.2022 13:08

      В статье это сказано: рассматривается разностная схема с сеткой размера 1000x1000, то есть по 1000 точек на каждом направлении. Количество шагов итерационного процесса для этой сетки до выполнения условия останова составляет примерно 428000. О применимости других методов я, честно говоря, не слышал и быстро найти не смог. Если Вас не затруднит, был бы рад ссылкам на литературу с обоснованием возможности получить приближённое решение требуемой точности.

      Мне неизвестно о существовании нужных функций в библиотеках типа MKL. Подчеркну, что сложность одной итерации в этом методе O(MN), то есть здесь нет умножения матриц или "обычного" решения СЛАУ.

      Касательно библиотек для Эльбруса: есть оптимизированная EML, отвественность за её разработку лежит на МЦСТ, соответственно, там применены более продвинутые техники, можете почитать в Руководстве.


      1. Andy_U
        24.01.2022 13:40

        1000x1000

        О, тут я не искал.

        Количество шагов итерационного процесса для этой сетки до выполнения условия останова составляет примерно 428000

        Ой.

        Если Вас не затруднит, был бы рад ссылкам на литературу с обоснованием возможности получить приближённое решение требуемой точности.

        Мне неизвестно о существовании нужных функций в библиотеках типа MKL

        Все даже чуть проще. Тут все линейное - хватит GMRES. И он есть в MKL:

        https://www.intel.com/content/www/us/en/develop/documentation/onemkl-developer-reference-fortran/top/sparse-solver-routines/iterative-sp-solvers-reverse-comm-iface-rci-iss/fgmres-interface-description.html

        Но проще, наверное, сначала в Питоне поиграться: https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.linalg.gmres.html


        1. shcher Автор
          24.01.2022 14:10

          А, понял. Я прошу прощения, это было недопонимание с моей стороны, успел забыть некоторые вещи. По сути, у меня и есть реализация метода наименьших невязок (который MINRES как частный случай GMRES, только здесь с более мягким условием останова).

          В рамках учебной задачи, естественно, нужно было реализовывать самому. Спасибо за наводку, интересно будет сравнить, как соотносится ручной вариант с использованием оптимизированных библиотек.


          1. Andy_U
            24.01.2022 15:19
            +2

            Для GMRES, особенно если с preconditioner'ом поиграть, число итераций будет очень сильно меньше. Естественно, сами итерации сложнее.

            Я уж было дернулся на Питоне проверить, но не понял, почему у Вас в функции F_border() в вычислениях участвует 1/DX, 1/DY (run_config->inv_h1, run_config->inv_h2)?


            1. shcher Автор
              24.01.2022 15:56

              Они появляются после выполнения преобразований над аппроксимацией граничных условий. Формула корректна, я проверял её. Сейчас не могу подробно написать, но вечером готов выписать формулы и обоснование.


              1. Andy_U
                24.01.2022 16:42

                Спасибо, просто добавьте, если можно, формулы прямо в статью. Может, кто еще захочет с Вами посоревноваться.


                1. shcher Автор
                  26.01.2022 21:12

                  Вышла небольшая заминка по времени, но теперь наконец добавил пример для правой границы под спойлер. Для остальных границ получается аналогично, в коде у меня по сути отражена та же самая формула, просто написанная не в TeX.

                  Да, кстати, пока писал, задумался о том, что "просто так" выразить задачу в матричном виде не получится: из-за шаблона "крест" придётся рассматривать неизвестный вектор размера MN, а матрица будет иметь размер MNxMN. Интересно, как справится общий метод при таких размерах.


                  1. Andy_U
                    26.01.2022 21:42

                    Спасибо. А матрица сильно разреженная, т.е. максимум 5 ненулевых элементов в строке, столбце...


                  1. Andy_U
                    26.01.2022 23:44

                    Вдогонку... Не нравится мне, что Вы с граничным условием сделали. Если бы просто написали (это для левой границы) аппроксимацию второго порядка, хотя это и лишнее, на самом деле:

                    \frac{dy}{dx}|_{x=x_0} = \frac {-y \left( x_{0}+2\,h \right) -3\,y \left( x_{0}  \right) +4\,y \left( x_{0}+h \right) }{2\,h}

                    но Вы вторую производную по X взяли из уравнения Лапласа (чтобы за границы массива не выйти?), но оно же на границе не работает, тут именно граничное условие справедливо! Ну и в углах беда.

                    P.S. И в одномерном случае ваш фокус невозможен.


                    1. shcher Автор
                      27.01.2022 00:34

                      Детали сейчас не помню, но без "фокуса с разложением в ряд" не получается второй порядок аппроксимации. Я, естественно, это всё сам не изобретал, а делал по описанию в задании, которое полностью соответствовало рассказанному на курсе по численным методам несколько лет назад. В одной из книг видел простое описание, сейчас быстро не нашёл, так что пока могу только предложить посмотреть страницу 190 из приведенной в статье книги, вроде это оно.


                      1. Andy_U
                        27.01.2022 02:02

                        Я привел формулу вычисления производной, которая дает точный результат для параболы, т.е. именно что второго порядка. Но, из практики, это может ухудшить сходимость. Тем более, что FEM нынче сильно популярнее. А на книжку гляну.


  1. virtusha
    24.01.2022 13:54
    +12

    Автор просто Мозг... человечество гордится такими людьми. Дай бог вам здоровья


  1. Civil
    24.01.2022 13:54
    +6

    К автору несколько вопросов/комментариев есть:

    1. Шаг "Векторизация" пропущен для Intel и Power. Кажется тут нужно добавить свидетельства, что компилятор успешно смог использовать AVX2 (ну или хотя бы SSE) и AltiVec соответственно.

    2. Даже если на (1) компилятор на первый взгляд справился на x86/ppc, то было бы неплохо посмотреть и оценить, насколько это хорошо сделано (проделать ту же работу, которая была сделана для Эльбруса).

    3. В целом, правила хорошего тона для публикации бенчмарков - публиковать краткую информацию как ими пользоваться. Это полезно, чтобы любой читатель мог пойти и воспроизвести результаты работы и независимым образом их проверить (или добавить еще машин для сравнения). В целом сюда же еще и ожидаемый вывод было бы неплохо приложить, чтобы у пользователя через эн лет на каком-нибудь совершенно другом железе не возникло вопросов "как проверить, что код до сих пор работает корректно?".

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


    1. shcher Автор
      24.01.2022 15:51
      +3

      1. Так ведь всё написано: "Заметим, что на рассматриваемых процессорах Intel и IBM нет требований к выравниванию данных, а потому компиляторы ещё на предыдущих этапах смогли справиться с векторизацией всех циклов". Думаю, из флагов оптимизации под текущий процессор (-xHost, -O5) понятно, что использовались AVX2 и 128-битные регистры на Power (не помню, как точно они сейчас называются, поэтому не пишу AltiVec).

      2. Много чего было бы неплохо сделать, но статья посвящена конкретно рассмотрению Эльбруса и ориентирована на него. Уверен, материалов по Intel и IBM можно найти намного больше и более полных.

      3. Это не бенчмарк, прошу материал в таком качестве не рассматривать. Что же касается запуска и ожидаемых результатов, эта информация будет оформлена в файле readme, который появится позднее.

      4. Спасибо, полностью согласен, это недосмотр с моей стороны! Добавил файл с лицензией.


      1. Civil
        24.01.2022 16:30
        +1

        1. Флаг компиляции никоим образом не гарантирует, что компилятор что-то векторизовал, тем более Intel C++ Compiler Classic это не самый популярный компилятор с не самыми очевидными наборами флагов (который тем более предлагается заменить на oneAPI C++ Compiler на базе LLVM), например я попросту не помню какие флаги там включают возможность использования AVX, так как последний раз с ним сталкивался лет 5 назад.
          Также с Power - флаги сборки не гарантируют что компилятор успешно векторизовал цикл, пока не доказано обратного. Ну и AVX2 - они таки 256-и битные регистры должен использвать.

        2. В таком случаи к статье фундаментальная претензия иного рода - вы потратили сколько-то (с виду не мало) времени на оптимизацию кода под одну платформу, но при этом не проделали такой же работы с другими. Без явного указания этих моментов (что статья не преследовала цели сравнить оптимизации на каждой платформе) создается ложное ощущение, что вы считаете код под PPC и Intel эквивалентно оптимизированным. То есть тут либо проделать такую же работу по ним, либо disclaimer в начало. Ну и ценность статьи соответственно падает.

        3. Хм... Это опять же не очевидно, так как в материале код явно используется как бенчмарк и сравнения платформ приводятся параллельно в одной сводной таблице.

        4. Спасибо за файл с лицензией.


      1. Civil
        24.01.2022 19:29

        Немного дополню почему я спрашиваю про векторизацию - я попробовал погонять код на ноутбуке (понятно, что сравнение не совсем корректно) и чуть-чуть поиграть с флагами сборки, как раз чтобы проверить векторизацию кода. Да, у меня на ноуткбе clang, а не Intel'овский компилятор, но у него в более подробном выводе про векторизацию циклов (-Rpass-analysis=loop-vectorize -Rpass=loop-vectorize -Rpass-missed=loop-vectorize) видно что не все из них векторизуются на -O3 и на Ofast ситуация чуть лучше (на O3 итерация занимает 2.9 секунды, на Ofast - 1.6с, без каких либо дополнительных изменений в коде и при сохранении того же самого вывода, то есть предполагаю что в таком случаи задача все же корректна решена).

        Отсюда вопрос, в том как хорошо интеловский компилятор векторизовал написанный код (это опуская вопрос почему был выбран Compiler Classic вместо их нового, да и в целом почему не сравнивали с gcc или llvm, которые немного более распространены среди простых людей)


        1. shcher Автор
          24.01.2022 19:53
          +3

          Я рассчитывал, что мне можно поверить на слово :) Собственно, никакого секрета здесь нет: наличие векторизации на ymm регистры я проверил по ассемблерному коду (есть вхождение тела цикла, соответствующее обработке по 4 элемента в ymm регистре), на IBM есть замечательная опция -qlistfmt, по ней создаётся подробный отчёт, в котором видно, что цикл был успешно векторизован и раскручен (на 2, если я правильно помню).

          Классический компилятор — это известный инструмент, с которым было понятно, как работать (в частности, касательно использования MPI), он продолжает поддерживаться, обновляться и поставляться в составе набора для HPC применений.

          Если честно, Вы хотите слишком многого от меня. Ещё раз повторюсь, было бы интересно всё происследовать и проверить, но статья не о сравнении компиляторов, а о методах оптимизации с прицелом на Эльбрус. Если бы я начал сравнивать с другими компиляторами, мне бы пришлось то же самое провернуть на POWER (как минимум, сравнить с gcc — он тоже есть на машине, к которой у меня доступ) и на Эльбрусе. Ну и во что превратилась бы статья?.. Выбор же icc как дающего лучшие результаты обоснован личным опытом и рядом других факторов. Ни в коем разе не претендую на абсолютную справедливость сделанного выбора.


          1. Civil
            24.01.2022 20:15
            -5

            Я рассчитывал, что мне можно поверить на слово :) 

            Если это в статье не упомянуто, то как я могу знать что вы это проверяли? Опять же, пример на базе эксперимента выше (да, с clang'ом) я привел - то есть я еще и дополнительно видел что не все что стоит векторизуется корректно с тем что привдено (правда да, на другом компиляторе, но у меня нет под рукой систем на разумном по новизне Intel'е).

            Если честно, Вы хотите слишком многого от меня.

            У меня это общая претензия к статьям подобного рода. Ваша - не первая. Я считаю что уровень материала претендует на интересность, но недостаточно высокий в целом, в виду недостаточных времязатрат на исследование. И чтобы оно было интереснее - нужно провести подобного уровня исследование и для x86 и в данном случаи для ppc. Исследование выбора компилятора тоже полезное свойство.

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

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


            1. shcher Автор
              24.01.2022 20:33
              +3

              Ну как же не упомянуто? Я уже и в комментарии Вам процитировал, вот ещё раз приведу выдержку из текста статьи: "компиляторы ещё на предыдущих этапах смогли справиться с векторизацией всех циклов".

              Давайте сойдёмся на том, что я не претендовал на "достаточно высокий в целом" уровень материала, и не будем говорить о недостаточности объёма трудозатрат. Объём определялся тем, что мне интересно, о чём я хотел поделиться, и соответствием теме статьи.

              Вроде из названия и текста чётко видно, что речь идёт о подходах к оптимизации на Эльбрусе. Что результаты на двух других архитиктурах приведены для "контроля адекватности" и демонстрации универсальности применяемых подходов, а не являются бенчмарком или сравнением процессоров/компиляторов. Хорошо, обещаю в следующий раз постараться не вводить Вас в заблуждение.


  1. nbkgroup
    24.01.2022 15:27
    +9

    Поучительно.

    Результат показал, что суперскалярная архитектура на порядок эффективней VLIW и экономит дорогой ресурс — рабочее время квалифицированных разработчиков, позволяя заменять их малоквалифицированными.

    В целом, история напоминает известный анекдот про вырезание аппендицита автогеном через анальное отверстие — можно, но трудно и требует высокой квалификации.


    1. shcher Автор
      24.01.2022 15:28
      +11

      Вы как-то одним махом отбросили замечательный (без сарказма, речь о многопоточной работе и SMT8) суперскалярный IBM, который изначально был всего в 5 раз быстрее. Он ускорился в 13 раз, это сложно назвать "экономит". Да даже разница в 4 раза в случае Intel может вылиться Вам в неприятный сюрприз по стоимости времени работы на вычислительном кластере.

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


    1. AKonia
      24.01.2022 16:24
      +4

      Ерунду пишете, т.к. на практике оптимизировать сложно и под интел и под ибм. Да и приведённые автором оптимизации для Эльбруса не какие-то там эвристики, а вполне обычные методы оптимизации кода, вспоминаем MKL, openblas, второй достоверно известно оптимизирован самым неприятным путём - написанием штучных реализаций на ассемблере.


  1. Tujh
    24.01.2022 17:02

    Круто, было бы ещё интересно посмотреть (спортивный интерес) на №0 с -O0


  1. tbl
    24.01.2022 17:57
    -1

    Оттюнили алгоритм для решения на матрицах размера 1000х1000, отлично. А завтра прилетят матрицы 1001х1001 или 1500х1500 - снова тюнить? А потом приедут симметричные, треугольные или разреженные матрицы со своими лэйаутами хранения данных в памяти - для них тоже отдельные пляски устраивать?


    1. shcher Автор
      24.01.2022 18:55
      +5

      Очень печально, что вы даже не попытались разобраться ни с решаемой задачей, ни с идеей статьи. Уверенно это заявляю, так как в противном случае вы бы начали с вопросов ("А как Ваши подходы применимы к другим размерам задачи?"), ну и тон был бы менее агрессивным.

      Итак, во-первых, все представленные оптимизации являются универсальными для размеров сетки, скажем, от 100x100. На сильно меньших размерах вряд ли понадобится запускать: при запуске в один процесс количество точек решения будет недостаточным, при запуске на много процессов возрастут накладные расходы на обмен данными. Где-нибудь сказано, что существенно используется размер сетки? В определённых местах можно было потребовать чётный размер, например, но этого не сделано, сохранён общий алгоритм. Вообще, все представленные оптимизации направлены на ускорение самой вычислительно объёмной части, которая в рамках одного шага метода составляет O(MN) для сетки размера MxN.

      Во-вторых, какое обоснование вас бы удовлетворило? Сделать запуски на всех размерах от 1001x1001 до 1500x1500? Это немного не научно.

      В-третьих, о каких таких матрицах особого вида вы здесь говорите? Я честно не понимаю. У меня разностная схема для уравнения Пуассона в двумерной области и метод наименьших невязок. А что вы придумали в качестве "решаемой в статье" задачи — решение обычной СЛАУ?

      В-четвёртых, да, если в какой-либо (любой, не этой) задаче появляются матрицы особого вида, то к ним применяют другие подходы. Начиная с асимптотически менее сложных алгоритмов, использующих структуру матриц, и заканчивая отдельными реализациями, эффективно работающими с представлением в памяти.


  1. alexeishch
    24.01.2022 18:19
    +1

    Выглядит так, как будто компилятор для Эльбруса дубовый. Компилятор от Intel банально более крутой. Еще когда я в 2007 году в Летней Школе Intel, он тогда за счет оптимизаций рвал всех. Кстати наверно, если ты учишься - то тебе будет полезно и интересно туда попасть. https://russia-students.ru/


    1. napa3um
      25.01.2022 09:45

      Itanium они своим компилятором (экспертизой в компиляторах) не спасли, рынок так и не прожевал VLIW. Может, сейчас и правда новое время, какой-нибудь LLVM уже дорос до возможностей реализации автоматических оптимизаций приемлемого уровня на подобных процессорах, или даже нейросетки какие-нибудь. Но, кажется, тут надо вложить в разработку компилятора сильно больше ресурсов, чем в сам процессор.


      1. anka007
        25.01.2022 10:46
        +1

        Интел в свое время всю команду компиляторщиков МЦСТ переманил. Не помогло. Ну и сейчас такой тип статической параллелизации не в тренде, производительность набирают специализированными исполняющими устройствами.


        1. napa3um
          25.01.2022 11:09
          +1

          Тем печальнее выглядят перспективы. Может, им перед своим VLIW-ядром припаять блок декодера-транслятора x86-команд? (Ну ладно, я помогал как мог, дальше пусть сами.)


          1. anka007
            25.01.2022 11:13
            +1

            Он там уже есть, эльбрус умеет выполнять х86 код, хоть и очень медленно.


            1. napa3um
              25.01.2022 11:15

              Сдаюсь :).


              1. anka007
                26.01.2022 12:00

                Думаете уже ничто не поможет? :)

                Эльбрус просто не надо позиционировать как универсальный. Надо "он отлично подойдет для того, сего, этого и еще 100500 пунктов". Но по каждому надо провести подготовительную работу по оптимизации и подгонке. Покупать отечественное так или иначе заставят, это уже понятно, но чтобы не портить репутацию не надо его ставить там, где простой пользователь без технической подготовки заметит очевидную разницу и там где могут запускать индусский код. Код тоже должен быть русским!


                1. napa3um
                  26.01.2022 14:24

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

                  Возможность запускать индусский код - это плюс, а не минус (хотя думаю, что это у вас был сарказм об "русском коде"). Надеяться на Великих Программистов (о которых, конечно, в ближайшее время мы услышим ещё много саг и легенд) в вопросах развития отрасли страны совершенно точно не стОит. Иначе у нас не процессоры будут, а автоматы Кемпелена, и теплопакет устройства будет рассчитываться с учётом заваривания трёх пачек доширака в день.


                  1. anka007
                    26.01.2022 16:09

                    Универсальный процессор должен уметь хорошо и быстро перемалывать универсальный код. Универсальный код в моем представлении очень близок к рандомному - там в любых пропорциях встречается что угодно. Как это сочетается с жестко заданным на момент написания/компилирования параллелизмом кода я не понимаю.

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

                    В 20м веке выбирали архитектуры, в нулевых растили частоту, в десятых наращивали параллелизм. Из этих подходов выжали почти все, что могли. И как распараллелить реальные "универсальные" программы тоже исследовали. Следующий этап, который уже вовсю применяется, - специализация. Процессор не должен быть универсальной молотилкой, он должен надежно и быстро связывать специализированные вычислительные блоки. Короче меня risc v приложило.


                    1. napa3um
                      26.01.2022 16:26

                      Это всё так идеилистически и абстрагированно звучит, что на реальность это всё ещё нужно постараться спроецировать :). А реальность, на мой взгляд, заключается в следующем: на фоне дефицита технических кадров и общего развала (отставания) отрасли в ней сформировались тренды, мало связанные с реальностью (экономикой), культивировался авторитаризм Бабаяна (бога церкви "неулучшаемых архитектур") и возникла сложность "продажи" "верхам" более прагматичных и практичных направлений разработки (если такие попытки вообще осуществлялись). RISC V, ARM, x86 стратегически неверно будет брать на роль архитектуры "национального процессора", но их подходы (и историю набивания шишек) совершенно точно стОило бы учесть.

                      Конечно, это всё диванная аналитика, сам я совершенно не в теме и сужу только по нескольким интервью (и по очень поверхностному пониманию процессоростроения). Но сложилось ощущение, что перспективы Эльбруса в риторике где-то "в верхах" очень сильно преувеличиваются. Такое ощущение, что основная ценность Эльбруса заключается в "аналоговнет".


                      1. anka007
                        26.01.2022 17:03

                        Бабаян между прочим почетный работник Интела! У нас еще есть Байкалы с ARMом и комдивы с MIPSом уже в кремнии. На подходе первые risc-v.

                        Уникальная ISA тянет за собой уникальную экосистему - компиляторы, среды разработки, библиотеки, операционные системы и т.п. Просто для начала разработки прикладных программ. Создать все это на должном уровне тоже не просто и не дешево. А без всего это процессор просто дорогой кусок кремния, каким бы он ни был.


                      1. napa3um
                        26.01.2022 17:05

                        Да, про Бабаяна я в курсе, оттого его аторитет ещё более устрашающ. И почему же сразу "уникальная" ISA, не надо сильно уникальничать. И уж точно это можно сделать менее уникальным, чем уникальность Эльбруса сейчас (он "уникален" именно из-за недостатка качественных компиляторов, и наращивать это качество Эльбрус обречён в одиночестве).


                      1. anka007
                        26.01.2022 17:18

                        Особенно если продавать наработки с коллективом всяким интелам...


                      1. Am0ralist
                        26.01.2022 17:21
                        +1

                        и возникла сложность «продажи» «верхам» более прагматичных и практичных направлений разработки (если такие попытки вообще осуществлялись). RISC V, ARM, x86 стратегически неверно будет брать на роль архитектуры «национального процессора», но их подходы (и историю набивания шишек) совершенно точно стОило бы учесть.
                        Наоборот. Попытки покупок чужих или ставших потом свободными архитектур не взлетели. x86 — не смешно, не купишь. АРМы успешно продали верхам Байкаловцы, но там лицензии и ограничения, что нельзя их в силовые продавать, например.
                        Риск5 только вот начал обсуждаться как замена армам и примеров крутых решений на них ещё нет, но Ядро (купленная одним олигархом вполне себе работающая контора) уже вроде приступило к разработке на нём, причём это вариант, который свободен от проблем АРМов, кстати.
                        Однако, на текущий момент Эльбрусы единственное что в рамках заданного из себя что-то представляет и глупо выкидывать их прямо сейчас, пока нет замены. Потому что они по сути не особо хуже тех же мипсов китайских, которые те себе пилят потихоньку. И к моменту выхода рисков у нас по идее могли бы быть уже 16 ядерные эльбрусы в продакшене, которые лучше 8CB, а то и 32 ядерные на подходе были бы.

                        Но вариант этот не для замены всего и вся. С другой стороны уже сейчас миграция на свободные ОС усилилась, что есть гуд. Если уж даже сапры стали портировать на линуксы — то лёд тронулся. А что в итоге будет под капотами — да отлично, если риск5 выстрелит и будет он. Но пока это очередные обещания, тогда как эльбрусы 16 уже инженерники в железе — есть.


                      1. napa3um
                        26.01.2022 17:22

                        Я не о покупке чужого, а о разработке своего (в интервью в том числе рассказывали об проблемах лицензирования и ARM, и x86, и даже бесплатного RISC-v для своих процессоров).


                      1. Am0ralist
                        26.01.2022 18:12

                        Так пытались. Мипсы, спарки и т.п. Своё с нуля — ну вот, эльбрусы.
                        Риск5 — это тоже своё будет. Так как просто набор команд, а как его реализуют — это уже дело Ядра.
                        Так что продаётся же ж верхам. Вопрос в том, что раньше то и «продавать» нечего было. Те же китайцы на мипсе почему делают один свой сейчас?


                      1. napa3um
                        26.01.2022 18:13

                        Я не о покупке (лицензировании) чужого, а о разработке своего. RISC-V, как я понимаю, тоже получится не свой, а MIPS, возможно, устарел морально (или с ним тоже лицензионные проблемы, на которые Китай просто забивает). Но я сварщик не настоящий, если что, и более предметно вряд ли смогу что-то сказать :).


                      1. Am0ralist
                        26.01.2022 18:32

                        (или с ним тоже лицензионные проблемы, на которые Китай просто забивает).
                        Мипс закопали по сути (притом до этого сделав опенсурс), а китай свою выпустил, которая как мипс, только другая и своя.
                        Я не о покупке (лицензировании) чужого
                        а я о том, что смысл делать аналогичный чужим? в вливе была идея и она даже в каких-то местах применима нормально.
                        а делать тот же арм, но свой… а смысл? наработки сообщества вы так же не будете использовать, будут проблемы совместимости и т.п.
                        с ним тоже лицензионные проблемы, на которые Китай просто забивает
                        Китай ради того, чтоб проблем с армом не было выбил у арма неподконтрольную арму дочку китайскую. А арм потом вдруг понял, что дочку и правда контролировать не может
                        То есть лицензии ему не пофиг. Уже давно
                        RISC-V, как я понимаю, тоже получится не свой
                        Он — ничей. То бишь — общий. Свободная лицензия для всех даром. Зато поддержка сообществом будет высокая. Реализацию же конкретную ты можешь купить у других (и зависеть от их «лицензии») или сделать самому. Расширить команды тебе так же никто не мешает.

                        Так вот, делать свой ради своего можно, если есть идея какая-то. В Эльбрусах — это ещё и защищённое исполнение, например. Крутая фича, которую пока полноценно не получается заюзать.


                      1. napa3um
                        26.01.2022 18:40

                        Смысл - в технологической и экономической независимости. И ещё в некоторых критериях про национальную безопасность (контроль технологических цепочек с точки зрения ИБ), там лучше послушать представителей МЦСТ, они вполне доходчиво этот вопрос раскрывают. И этот смысл вполне себе объективный, конкурировать и на мировой арене имеет смысл даже в уже существующих дисциплинах, имеет смысл завязывать на себе производственные процессы и разработку. Свободная лицензия - это тоже лицензия. Тоже отсутсвие контроля над развитием отрасли и риск "распыления" стандартов. Китай это всё уже понял.

                        Глобализация - она только в комиксах такая, где добрые американцы (и связанные капиталы) раздают технологические плюшки, выстраданные кровью и пОтом, остальным голодранцам "во имя добра" :).

                        Но я не могу поддержать этот разговор на более предметном уровне, и так уже от вопросов "VLIW vs RISC/CISC", в которых у меня мало компетенций, меня затащили в глобально-политические, в которых их ещё меньше :).


                      1. Am0ralist
                        26.01.2022 22:49

                        мипс стал свободным, китай не отвалился.
                        риск5 свободный, ровно потому, что закрытый арм уже есть.
                        риск/циск вообще не приделах, ибо чистых не существует

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


                      1. napa3um
                        26.01.2022 22:55

                        Вы озвучиваете какие-то поверхностные и субъективные оценки и слегка наивно персонализируете стороны. Я не смогу поддержать этот разговор. Вряд ли мы с вами родим тут новые истины или повлияем на развитие проектов МЦСТ хоть каким образом :).


                      1. Am0ralist
                        27.01.2022 10:01

                        Ну, я стараюсь объективно подходить и против хейтеров эльбрусов, и против восхваливания его как единственного и неповторимого. Эльбрус объективно есть и на уровне прочих «замещений» мировых. Риск5 вроде заметно выше малинки не выходил, так что надо смотреть реальность.
                        АРМы как замещения — не работают полноценно, x86 — невозможно купить. Создание аналогичной закрытой системы — не несёт смысла, там каждые +5% — это миллиарды вложений могут быть и это будет не от «набора команд» зависеть, а от инженерных умелок реализации в железе. Эльбрусы то хоть развивались на старом базисе, зачем заново в эту же ловушку попадать.

                        Открытую — а смысл, кто её использовать будет, если есть другие, мировые.
                        А обеспечивать

                        национальную безопасность (контроль технологических цепочек с точки зрения ИБ),
                        можно на любом наборе команд, которую будут реализовывать здесь. в этом плане эльбрус не лучше риска5 будет.
                        Повторюсь, у эльбруса есть фишка с защищенным режимом, но вот программно ядро линукса не смогли пока к подобному адаптировать. И это когда вы уже переиспользуете кучу чужих наработок, создавать всё с нуля — это пупок развяжется. Лучше эти силы потратить на развитие самой местной микроэлектроники, для улучшения ИБ.


                      1. napa3um
                        27.01.2022 10:46

                        Я стараюсь вообще не оценивать эмоционально, для меня Эльбрус не хороший или плохой, просто вот есть такой. Это не хорошо или плохо, мне от этого никак, я с ним не пересекаюсь ни по работе, ни в быту. А советы куда там лучше или хуже потратить силы МЦСТ (которые, конечно, им очень нужны :)) - уже высказал с дивана, с "высоты" своего понимания функций государства и устройства процессоров :). Мнение не совпадает с вашим, судя по всему, ну, такое вот мнение, и бох с ним :).


      1. alexeishch
        25.01.2022 12:23

        Там была проблема выбора. Весь этот зоопарк очень дорого поддерживать. Даже Intel(!) была не в состоянии по деньгам поддерживать одновременно x86, x86_64, Itanium и ARM проекты.


        У Интела был прекрасный Итаниум, у Интела был прекрасный XScale ARM.
        Первый угробили в угоду x86_64, XScale угробили в угоду Silverthorne (который стал называться Atom). В 2007 году казалось что Silverthorne возьмет всю мобильную нишу. Нишу графических адаптеров возьмёт Larrabee (поэтому у стажеров тогда NDA был на три года). Т.е. абсолютно везде будет x86 )))

        В итоге эти наполеоновские планы не сбылись. Atom взял нишу нетбуков/субноутбуков, ARM стал подавлющей архитектурой на мобилке, а лидерство в графических адаптерах захватила NVidia


        1. napa3um
          25.01.2022 13:17

          Именно в "проблемах выбора" и проигрывается/выигрывается рыночная конкуренция, а Эльбрус даже до "выборов" ещё не дошёл :).


  1. iboltaev
    24.01.2022 20:34
    -1

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


  1. kovserg
    25.01.2022 00:14
    -1

    А вы попробуйте вместо 1000*1000 сделать сетке 800*800 и посмотрите разницу.


    1. kovserg
      25.01.2022 23:42
      +1

      Интересно чего минусуете. У intel 12Mb кэша, а у эльбраса 16Mb. Если слегка уменьшить сетку в задача у intel-а тоже поместиться целиком в кэш. Или же увеличите размер, что бы у обоих данные значительно превысили L3 кэша. Иначе получается очень предвзятое сравнение у эльбруса как раз влазим в L3 а у intel нет.


      1. shcher Автор
        26.01.2022 00:03

        Могу предположить, что минусуют вас за форму подачи и бессодержательность первого комментария. Написали бы сразу развёрнуто, был бы разговор, а то я вот просто проигнорировал первый комментарий, так как уже давал развёрнутый ответ на похожий. Теперь же мне есть, чем дополнить.

        Всего в алгоритме участвует 5 основных матриц, так что кажется справедливым смотреть на то, чтобы они все поместились в кэш одного уровня. При размере 1000x1000 это 38 МБ, не помещается в L3 ни на Intel, ни на Эльбрусе.


        1. kovserg
          26.01.2022 00:35

          Просто вы же полный код не привели. У вас явно фигурирует фрагмент src->dst и вызывается пара функций q и F и потом итерируется до сходимости, по остальные матрицы ни слова. И потом заявлено что количество итераций 428000.
          Т.е. 42800*1000*1000*8/5c ~ 637Gb/s а такая скорость только у кэшей, у памяти на порядок меньше.


          1. shcher Автор
            26.01.2022 00:42

            Вынужден обвинить вас в тотальной невнимательности. Полные исходники размещены на github, об этом сказано в статье, дана ссылка, потом внизу ссылка продублирована. А дальше ещё хуже: я пишу, что замеряется время выполнения 1000 шагов, а вы мне сейчас приводите непонятный расчёт скорости памяти. Мне кажется, лучше сначала читать статью, а потом идти в комментарии (да-да, только в комментариях упоминается 428000, это вас выдаёт).


            1. kovserg
              26.01.2022 00:52

              Точно был не внимателен.


  1. piratarusso
    25.01.2022 10:04
    +2

    Очевидно, что Эльбрус в существующем виде без существенной переработки компилятора будет показывать очень слабые результаты в смысле производительности. Ручная оптимизация при массовом применении процессора представляет серьёзную проблему. Практически плохо решаемую.


    1. anka007
      25.01.2022 11:36

      Потому что одного компилятора для современной разработки очень мало. Что пишут на "голом" Си или С++ в блокноте? Как у Эльбруса с инфраструктрой обстоят дела? Со стеком разработки?


      1. piratarusso
        25.01.2022 12:47

        Линукс портирован, значит C/С++, GNU make должны работать. В приниципе после этого можно портировать какой-то стек. Возникет закономерный вопрос. . Где потребители этой продукции. И зачем этим вообще заниматься.


        1. anka007
          25.01.2022 13:12

          линукс + gсс + x86 и уже можно как-то работать. А хочется не как-то, а нормально или даже хорошо. В специализированном применении можно и нужно оптимизировать определенный набор софта под конкретную платформу, и работать с этим будет специально обученый человек. А в универсальном применении пользователь айфонами избалованан, нужно чтобы минимум нормально работало "из коробки". До этого как до луны пешком.

          Возможно на специализированных серверах оптимизируют и поднимут определенное ПО. Но цену системы в комплексе все эти действия поднимают существенно - и железо дорогое, и софт дорогой, и поддержка дорогая, да еще по скорости не ахти.


          1. piratarusso
            25.01.2022 14:04

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


            1. anka007
              25.01.2022 15:05

              Микроэлетроника в современном мире стратегически важный ресурс. Развить и поднять отрасль - не так уж просто, это всем понятно. И сам по себе эльбрус не плох, но пока на звание универсального не тянет.

              Сейчас модно хендмейдом заниматься. Частные лица хлебопечки заводят да смеси для них покупают. Буханка раз в 5 дороже выходит. А у государства масштаб покрупнее - заводики для выпекания микропроцессоров. Тенденция такая, мировая.


  1. nixtonixto
    25.01.2022 10:35
    -2

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

    Для тестовых замеров будем смотреть на время выполнения 1000 шагов итерационного метода на одном ядре процессора

    Сила современных процессоров — в их многоядерности и взаимодействии между ядрами и кэшем. Распараллелив вычисления между всеми ядрами, разница между процессорами будет совершенно другой.


  1. Abbadosha
    25.01.2022 11:01

    А если провести обратный эксперимент и попробовать код оптимизировать для любого из 2х других процессоров, вот это будет интересно)


  1. ErmIg
    25.01.2022 11:44
    +3

    Как мне кажется задача упирается прежде всего в пропускную способнось памяти, а не в вычислительные возможности процессора. Intel Core i7-9700K имеет теоретическую производительность 50 GFLOPS на ядро (FP64) для FMA. В вашем цикле порядка 10 операций умножения и сложения. Значит теоретически он может выполняться раз в 10 быстрее (~500 мс). Все упирается в пропускную способность памяти, а не в векторизацию и т.п. Это к стати ответ, почему Эльбрус почти догнал гораздо более высокочастоный Intel и Power.


  1. Armmaster
    25.01.2022 14:20
    +2

    Примечательно, что на Эльбрусе, который имеет фиксированные и
    относительно большие штрафы за вызов функций, ускорение составляет 2.2
    раза, а на Intel и IBM — 2.9 и 1.5 раза, соответственно. Так что на
    предсказатель переходов и внеочередное исполнение надейся, а сам не
    плошай

    Тут некорректное сравнение. Инлайн функции в Интеле (про IBM не знаю) позволил сделать автовекторизацию, отчего и получился такой привар почти в 3 раза. Т.е. если хочется померять ускорение непосредственно от инлайна для Интел, надо выключить опциями векторизацию. Либо тогда уже сравнивать все улучшения от оптимизаций для Эльбруса вплоть до векторизации (т.к. без инлайна они всё равно были бы невозможны), что будёт равно 223.6/6.19 =~ 36 раз

    В целом, хотя статья очевидно направлена на описание оптимизаций Эльбруса и требовать таких же заоптимизаций для x86/IBM неправильно, но приведённые таблички могут легко ввести в заблуждение неподготовленного читателя. Например, как приведённый пример с векторизацией. Исходя из табличек может сложиться впечатление, что векторизация на том же Интеле вообще ничего не даёт (3.67->3.67), хотя в реальности векторизация применилась на шаге инлайна.

    Также было бы здорово привести хотя бы конечные версии ассемблерного кода для x86/IBM. Потому что лезть пересобирать, запускать и т.д. уже как-то облом, а вот беглый взгляд на ассемблер может дать много ответов на интересующие вопросы. Например, у меня есть подозрение, что у Интела не включилось fma, но исходя из данных в статье это сложно установить.


  1. DustCn
    25.01.2022 21:24

    Используйте -qopt-subscript-in-range для странных счетчиков с интеловским компилятором и будет вам счастье


  1. dzavalishin
    26.01.2022 03:28
    +1

    Замечательная статья. Читал запоем! Спасибо. :)