На Хабре уже было тестирование Эльбрусов на разных языках программирования (например, здесь). И данный обзор стоит рассматривать как дополнение, с ещё одним тестом, новыми версиями компиляторов и новыми участниками (Rust, С++). Также обзор сделан с упором на тест возможностей именно компиляторов и настройки оптимизации.
Предыстория
Однажды получив доступ к серверу на базе Эльбрус 8С, появилось желание оценить эффективность его работы с Java. Я уже был осведомлён о сложности JIT-компиляции на e2k и о проделанной ребятами из УниПро работе. Но, сделав замеры, их нужно с чем-то сравнить. Для этого хорошо подходят C++ и Rust, компиляторы которых должны лучше справляться с оптимизацией под e2k. Особенно C++, компиляции которого уделено большое внимание в рекомендациях МЦСТ. В качестве тестов был выбран расчёт простых чисел с помощью решета Эратосфена (блочный вариант). Теоретически, алгоритм должен хорошо оптимизироваться под e2k. И тем интереснее посмотреть, как с этим справятся компиляторы на разных языках.
Тестовые стенды:
x86:
AMD FX-6300@3500 Мгц. (турбобуст отключен).
Intel Celeron (Haswell) G1820@2700 Мгц.
Софт:
Ubuntu 22.04.
Java: OpenJDK Runtime Environment (build 11.0.25+9-post-Ubuntu-1ubuntu122.04).
Rust: rustc / cargo v.1.83.0; LLVM version: 19.1.1.
C++: GCC v11.4.0; LLVM version 19.1.5.
e2k:
Эльбрус 8С@1200 Мгц.
Софт:
Elbrus Linux 7.2
Java: OpenJDK Runtime Environment (build 11.0.15-Unipro+0-adhoc.root.openjdk11-11.0.15).
Rust: rustc / cargo v.1.57.0.
C++: lcc:1.26.22:Jan-10-2024:e2k-v4-linux (gcc (GCC) 9.3.0 compatible)
Испытуемые: Java, Rust, C++(GCC, LСC).
Тестовая задача
В качестве теста выступает решето Эратосфена в блочном варианте. Один поток. На трёх языках реализовано максимально идентично. Программа в консольном варианте. Есть возможность повторного расчёта.
исходник и jar-архив Java;
исходник и исполняемые (Win, Linux) файлы (+ ассемблер) Rust;
исходник и исполняемые (Linux) файлы (+ ассемблер) C++.
Методика тестирования
Выполняем два запуска по пять прогонов поиска простых чисел в диапазоне 0 - 5*108. Первый прогон прогревочный и в расчёт не идёт. Для Java прогревочный проход обязателен. И, как показала практика, на C++ и Rust первый прогон тоже чуть медленнее. По оставшимся результатам двух прогонов вычисляется средний показатель.
Описание опций тестирования
gcc O0:
$ g++ -march=native main.cpp -o eratosthenes_O0
gcc O2:
$ g++ -O2 -march=native main.cpp -o eratosthenes_O2
gcc O3:
$ g++ -O3 -march=native main.cpp -o eratosthenes_O3
gcc O4:
$ g++ -O4 -march=native main.cpp -o eratosthenes_O4
gcc O4 +fast-math:
$ g++ -O4 -march=native -ffast -ffast-math main.cpp -o eratosthenes_O4fast-math
gcc O4 +fast-math +PGO: двухфазная компиляция.
1-я фаза.
g++ -O4 -march=elbrus-v4 -ffast -ffast-math -fprofile-generate -Wall -c -fmessage-length=0 -MMD -MP -MF"pgo-1.d" -MT"pgo-1.d" -o "pgo-1.o" "main.cpp"
g++ -fprofile-generate -o "pgo-1" pgo-1.o
Сбор статистики:
$ ./pgo-1
с диапазонами: 100 миллионов; 500 миллионов; 2 миллиарда.2-я фаза.
g++ -O4 -march=elbrus-v4 -ffast -ffast-math -fprofile-use -Wall -c -fmessage-length=0 -MMD -MP -MF"pgo-1.d" -MT"pgo-1.d" -o "pgo-1.o" "main.cpp"
g++ -fprofile-use -o "eratosthenes_O4fast-math+PGO" ./pgo-1.o
gcc O4 +fast-math +PGO +long_int: Все типы
int
в коде заменены наlong int
;перед циклом с вызовом "
makeHoles(&block, curPnIdx);"
ставим "#pragma swp"
.Затем выполняем двухфазную компиляцию.
1-я фаза.
g++ -O4 -march=elbrus-v4 -ffast -ffast-math -fforce-loop-apb -fforce-vect -fforce-swp -fprofile-generate -Wall -c -fmessage-length=0 -MMD -MP -MF"pgo-1.d" -MT"pgo-1.d" -o "pgo-1.o" "main.cpp"
g++ -fprofile-generate -o "pgo-1" pgo-1.o
Сбор статистики:
$ ./pgo-1
с диапазонами: 100 миллионов; 500 миллионов; 2 миллиарда.2-я фаза.
g++ -O4 -march=elbrus-v4 -ffast -ffast-math -fforce-loop-apb -fforce-vect -fforce-swp -fprofile-use -Wall -c -fmessage-length=0 -MMD -MP -MF"pgo-1.d" -MT"pgo-1.d" -o "pgo-1.o" "main.cpp"
g++ -fprofile-use -o "eratosthenes_O4long_int+PGO" ./pgo-1.o
Результаты тестов
Некоторые неожиданности.
Опция -march=native
ни на одной из платформ не дала профита по сравнению с -march=x86-64
. При этом исполняемый файл, полученный с такой опцией на AMD FX, на Intel Celeron (Haswell) выпадал с ошибкой: "Недопустимая инструкция (образ памяти сброшен на диск)".
gcc/lcc O0 | gcc/lcc O2 | gcc/lcc O3 | gcc/lcc O4 | lcc O4 +fast-math | lcc O4 +fast-math +PGO | lcc O4 +fast-math +PGO +long_int | Rust | Rust | Java | |
Elbrus 8C @ 1.2Ghz | 45712 | 4694 | 3912 | 3830 | 2479 | 2149 | 1968 | 4941 | 5033 | 19357 |
AMD FX @ 3.5Ghz | 7743 | 2373 | 2208 | 2205 | -- | -- | -- | 1818 | 1918 | 4635 |
Cel G1820 @ 2.7Ghz | 7508 | 1648 | 1523 | 1543 | -- | -- | -- | 1183 | 1213 | 5123 |
Пояснение к результатам тестов
Жирным выделены результаты самых удачных настроек компиляции для каждой машины.
Некоторые опции компиляции, указанные в таблице, не дали никакого прироста к производительности на x86. По этой причине полноценное тестирование с этими опциями не проводилось и данные в таблице не заполнены.
Обозначение gcc/lcc означает, что x86 использовался компилятор gcc, а на e2k - lcc (компилятор МЦСТ, совместимый с gcc).
Сравним на графике результаты компиляции C++. Сразу бросается в глаза разница между lcc O0 и lcc O2 на Эльбрусе. Такова плата за запуск неоптимизированных программ на e2k.

В отличие от x86, для Эльбруса оптимизация O4 даёт профит.

( ! ) К сожалению, на период проведения тестов и написания статьи двухфазный режим компиляции для Rust не доступен. Как появится такая возможность, результаты будут добавлены в таблицу.
Таким образом, для Rust везде лучший результат показала оптимизация O2.
Итак, лучшие настройки оптимизации для данного теста на платформах:
x86: Rust - O2, C++ - O3.
e2k: Rust - O2, C++ - O4 +fast-math +PGO +long_int.


P.S.
Не являюсь специалистом по Rust и C++. Код достаточно простой и там вряд ли будут серьёзные недочёты. Но мог недоработать в плане настроек компиляции.
Благодарю компанию МЦСТ за возможность ознакомления с ЦП семейства Эльбрус!