В этот раз могу так: взять любимый язык Pascal и на нем что-нибудь наваять. Наваять захотелось какую-нибудь мерилку производительности процессора. Люблю, я, понимаешь, всякими числами всё измерять.
Понятно, что мерилок таких понаделано и без меня не один вагон. Но все же хочется чего-то своего. Думал, думал, как мерить, и не придумал ничего лучше, чем решением систем линейных уравнений методом Гаусса-Жордана. Не знаю, почему именно этот метод мне понравился, но вроде норм.
Такой подход хорош тем, что можно замерить не только систему команд SISD, но и SIMD, смотря как реализовывать. SIMD мне давно хотелось попробовать, но все руки никак не доходили, да и там на x86/x64 такой зоопарк из инструкций, что просто так, без бутылки, к ним лучше и не лезть.
Ну, то есть вы поняли уже, да? Я не только Pascal люблю, но и Assembler в лайтой версии тоже. Видимо, детская травма суровым советским бытом: калькулятор MK-61 и вот всё это вот 😉.
Короче, накидал быстренько тестовый код на паскале, проверил, что все корректно работает. Правда, жизнь себе немного упростил, считая, что случайно сгенерированные системы будут всегда совместными и определенным.
Дальше мне стало скучно и я переписал решение на ассемблере. Сначала начал для чисел расширенной точности, в терминах Pascal это тип extended или, для краткости, буду здесь его называть FP80 (потому что занимает 80 бит).
В результате получил небольшое ускорение. Примерно в 2.3 раза для маленьких систем с тремя неизвестными, и в 1.12 раза для больших систем в 768 неизвестным. Ну, думаю, не такой уж большой выигрыш, что бы переписывать на ассемблере код.
Думаю, для более коротких вещественных типов выигрыш будет, наверное, еще меньше. Но все же реализовал ради забавы. И оказалось, что я ошибался. Для типа с двойной точноностью (double или FP64) выигрыш оказался 1.76 раза для маленьких и 3.44 раза для больших систем (1536 неизвестных).
Для типа одинарной точности (single или FP32) выигрыш составил 1.7 раза для маленьких и 5 раз для больших систем (1536 неизвестных). Конечно, я город таким достижением 😀, хотя на самом деле это не моя заслуга, а Delphi, так как ходят слухи, что её последние версию не очень хороши в оптимизации.
Кстати, заметна разница в производительности между FP80 и остальными. Это связано с тем, что в FPU нет команд для операций с FP80 прямо из памяти. То есть приходится загружать оба операнда в регистры, проводить операцию между регистрами, потом сохранять. Примерно так:
fld tbyte ptr [eax];fmul st(0), st(1);
fstp tbyte ptr [eax];
А для FP32 это выглядит чуть по-другому:
fld st(0);
fmul dword ptr [eax];
fstp dword ptr [eax];Вроде команд столько же, но работают они гораздо быстрее, такая уж особенность архитектуры: при выполнении операции с операндом в памяти загрузка из памяти и расчет значения производятся, похоже, одновременно.
Ну и сказывается, конечно, то, что 10 байт плохо выравниваются, что снижает эффективность и кэширования, и операций с памятью.
Предварительные результаты: на стареньком бюджетном AMD FX-4350 частотой 4.2 ГГц и памятью на 1600 МГц процессор решает 3.8 млн. систем FP80 из 3-х линейных уравнений за 1 секунду, 10.7 млн систем FP64 и 10.8 млн. FP32.
Правда, сложность решения пропорциональна кубу количества переменных, так что максимум, что можно решить за одну секунду, составляет в районе несколько сотен переменных FP80 (< 700), и чуть больше 1000 для FP32.
Много это или мало? Смотря для чего. Скажу так, что 30 лет назад о таком и мечтать было нельзя, причем не только в г. Новокузнецке. Но, например, при расчетах, связанных с моделированием реального мира, это не много. Например, при расчете модели трехмерного мира модным ныне методом трассировки лучей, где используются не совсем такие, но близкие к этому методы, такая производительность даже близко не позволит подойти к получению высококачественной картинки в реальном времени.
Ну а я пока продолжу развлечение. Если будет что интересное - напишу.
Интересно Вы время проводите, решил и я попробовать написать эту числодробилку на C, чтобы посмотреть на что способен clang в плане оптимизиции, тестировал на i5-7267U CPU @ 3.10GHz.
ОтветитьУдалитьДля систем из 3-х уравнений цифры сравнимы с Вашими ~10 млн. систем за секунду, в целом то что и ожидалось: для маленькой системы ни развороты цикла, ни SIMD инструкции не помогают.
Для больших же систем цифры по интереснее.
Без оптимизации моя реализация решает систему с 900-1000 неизвестными, посмотрел сгенерированный ассемблер для 3-го вложенного цикла: компилятор использует 128 битные регистры и SSE инструкции, но кладет в регистры только по одному числу, никакой магии, но и криминала не обнаружено.
С оптимизацией и разрешением avx инструкций, обычный C код, компилятор развернулся по полной: 256 битные регистры используются полностью, да еще и внутренний цикл развернут: 4 итерации обрабатываются в теле цикла (16 double или 32 float). Как результат: система из 1600 неизвестных для double или 2000 для float за секунду. Для внутреннего цикла ускорение аккурат в 4 раза для double и 8 раз для float получилось. Получается не зря они эту гору инструкций добавили.
Правда жалко что компилятор не стал использовать multiply-add инструкцию для внутреннего цикла, пришлось вручную переписать цикл на использование соответствующего intrinsic, это увеличило число переменных до 2300 для float и 1800 для double.
Наверное если переписать на CUDA, размерность увеличиться на порядки, но с этим мне не приходилось сталкиваться, как я понимаю там все совсем по другому.
А Вы не пробовали Free Pascal? Он вроде все еще развивается, может у него все получше с оптимизаций чем у Delphi. Они там вроде с llvm экспериментируют :)
Нашел баг, после исправления оказалось что вручную добавленная multiply-add не дает никакого прироста, по крайней мере на моем процессоре, и без нее система из 2300 float решается за секунду. Кстати нашел интересный сервис позволяющий посмотреть результирующий ассемблер для разных компиляторов, а еще он позволяет поделиться ссылкой с кодом. В общем вот моя реализация если интересно https://godbolt.org/z/v8rhf1
УдалитьЕму кстати можно паскаль скормить и скомпилировать используя free pascal :)))
Да, мощный компилятор - это круто! Есть подозрение, что AVX - это наконец-то нормальный набор универсальных SIMD инструкций и там гораздо проще развернутся, чем в SSE, где инструкции больше заточены под конкретные задачи обработки мулььимедиа. Но я пока до SIMD еще не добрался в своих развлечениях.
Удалить>А Вы не пробовали Free Pascal?
Нет пока. Возможно, когда-нибудь и попробую. Новые версии Delphi тоже регулярно выходят, я как бы к ним уже привык. Ну и меня прикалывает часть кода писать чисто на ассемблере, поэтому именно за супероптимизацией не гонюсь.
У меня есть подозрение, что i5-7267U должен гораздо быстрее работать на системах из 3-х уравнений, чем мой AMD. Но я делал 32-битную версию, может быть в этом дело.
УдалитьКод посмотрел. Несколько моментов: деление лучше заменить умножением, когда мы приводим к 1 крайне левый коэффициент верхней строки. Компилятор не сообразил/не захотел. А может, нет выигрыша от такой замены? Хотя сомневаюсь.
Я перед замером готовлю массив из разных СЛАУ. Ну и в силу этого также провожу выделение главного элемента на каждой прямой итерации в методе Гаусса-Жордана, иначе для некоторых случайных наборов переменных ошибка оказывается слишком большой.
Про SSE vs AVX сильно не разбирался, но clang использует их все если разрешается, с AVX правда чуть-чуть быстрее :)
Удалить32 vs 64 bit не знаю, в целом у меня в коде почти нет указателей, без SIMD инструкций и много-поточности все должно быть очень близко как мне кажется.
Компилятор не может заменить вещественное умножение на деление, так как результат операции может быть чуть-чуть другим, но это ему можно разрешить (-ffast-math опция), правда заметного выигрыша я не увидел, так как основное время внутри 3-го вложенного цикла.
Я тоже генерирую все случайные системы перед тестом, правда они у меня все в одном одномерном массиве, а дальше игра с индексами.
Ошибка и впрямь огромная. Добавил поиск максимального (по модулю) элемента и перестановку строк. Для double - без изменений, система из 1600 уравнений за секунду (что ожидаемо), а вот для float - теперь только 1700 уравнений, похоже раньше очень быстро накапливалась ошибка и это позволяло компилятору умножать в разы быстрее. Другого объяснения я не вижу.
Явно указателей нет, но ассемблер практически везде использует косвенную адресацию вида
Удалитьvmovss xmm0, dword ptr [rcx + 4*rbx - 4]
Возможно, что в 32-разрядном варианте такая адресация должна быть чуть полегче для процессора.