Lazarus - это IDE а-ля Delphi, только под Free Pascal (FPC). Установил, на первый взгляд все было очень ничего. Внешний вид очень похож на Delphi 7.0 - самый удобный интерфейс для разработки. Тот интерфейс, что появился в более поздних версиях Delphi, лучше использовал площадь широкоформатного экрана, но был гораздо менее удобен.
Единственно, что удивило, в настройках интерфейса по умолчанию вся пунктуация (это называется "языковые символы") - красного цвета. Жесть.
К сожалению, на второй и третий взгляд все оказалось не айс. В конфах пишут, что умеет в AVX прямо с паскаля конвертировать, но слегка с глюками, то есть все операции с массивами должны быть кратны 4, иначе не компилит. Не знаю, не проверял.
Меня интересовало как спортируется код Delphi и как ассемблер встроенный там пашет. Портировалось нормально, с минимальными переделками все даже заработало. Но на паскале.
С ассемблером оказалось сложнее. Споткнулся компилятор вот на таком фрагменте:
procedure SolveTestSingleLESP_SSE(var P : TLESInfoP; LESNum : UInt64); // P - RCX, LESNum - RDX;
asm
// R8 - P, R9w - n, R10w - index
push RBX;
mov R8, P.Systems;
movzx R9, word ptr P.LESSize; // в R9w - n;
mov R8, [R8+LESNum*8]; //
...
Параметры передаются в регистре RCX и RDX соответственно (это, оказывается, в Windows так принято, поэтому работает одинаково что в Delphi, что в FPC). И если с первыми строчками все нормально, то вот с последней компилятор забавно загнал. Он правильно распознал, что LESNum находится в RDX, но почему-то транслировал после замены переменной на регистр в mov R8, [R8*8+RDX].
Если переменную заменить явно на имя регистра, то транслирует нормально.
Второй затык случился на команде такого типа:
movss XMM0, dword ptr c_single1[0];
Почему-то компилятор выдал предупреждение, что он сгенерировал абсолютную 32-битную ссылку на константу, поэтому программа обязательно должна быть загружена в младшие четыре гигабайта ОЗУ!
Едрит-Мадрид! Это как и почему? Вроде же используется виртуальная память, которой все равно, где физически располагается переменная. И зачем специально генерировать абсолютную ссылку?
Естественно, в этом месте возникает ошибка. Наверное, это можно как-то порешать, используя какие-то специальные конструкции для ассемблера, имеющиеся в FPC. Но мне стало скучно и лень. Тем более, нормальной документации нету, что бы можно было легко докопаться до таких тонкостей.
Отдельно про debbuger. Я выяснил, что в Delphi он глючит, когда FPU (x87) занято данными более чем наполовину (просто данные верхней, самой новой половины стека не показывает). В Lazarus такого глюка нет. Просто потому, что там нет ресурсов FPU в отладчике как такового.
Ну и вообще там очень хиленькие и неудобные возможности по отладке в ассемблере по сравнению с Delphi. Регистров AVX там, кстати, тоже нет.
Синтаксис AT&T ассемблера отдельно достает. Нет, если впитал его с молоком матери, то нормально. Но у меня обратная ситуация. И если при разработке можно переключить ассемблер на синтаксис Intel, то отладчик всегда показывает только AT&T синтаксис.
В целом Lazarus произвел впечатление полудоделанного и слегка подзаброшенного продукта.
В общем, плюнул я на это дело и решил попробовать запасной вариант: написать нужные мне процедуры на чистом ассемблере.
Идеальный для меня вариант - TASM. Но беда в том, что Embarcadero его давно забросил. И поэтому он поддерживает ровно тот же набор команд, что и компилятор Delphi: то есть AVX там нет.
Почитал в инете, вроде прямым наследником TASM, который более-менее развивается, является FASM. Это как бы и так, но не совсем. FASM настолько далеко ушел в своем развитии, что я не смог сгенерировать объектный файл с самыми простыми процедурами. Отказывается последняя версия FASM делать такой объектный файл. Хотя вроде долго смотрел в примеры, писал все по образцу, но не получается. Правда, не догадался попробовать сами примеры скачать и скомилировать... Надо будет как-нибудь ради прикола сделать.
Короче, заменил процедуры метками, благо, у меня все передается в регистрах, а локальные переменные мне не нужны. Видимо, если бы были нужны, надо было стек вручную создавать? Ну да бог с ним.
Быстренько скопировал код SSE, адаптировал его под AVX и на типе single (float), удивительное дело, все правильно сработало с первого раза, без отладки. Но, ради интереса, решил посмотреть под отладчиком. И не смог: при попытке зайти в процедуру с кодом под AVX отладчик Delphi просто "умирает".
Так что отладки под AVX у меня нету. С кодом под double переход был не таким гладким, в паре мест накосячил, пришлось повозится, выискиваю баги без отладчика. Но тем не менее все получилось.
Теперь осталось собрать тестер и оттестировать доступные мне процессоры. Но предварительно уже могу сказать: на PileDriver код на AVX немного медленнее, чем на SSE. Впрочем, это ожидаемо: давно известно, что PileDriver очень медленно выполняется обращение к памяти по 256 бит. А большинство команд AVX именно такие.
И даже команды FMA PileDriver не помогли. Работает ровно с той же скоростью, что и просто AVX. Но точность чуть выше, чем отдельно умножение + сложение. Впрочем, для метода Гаусса-Жордана повышение точности не очень существенное.
Наверное, если использовать команды AVX на половинных регистрах (XMM вместо YMM), то может быть AVX слегка и обгонит SSE на моем FX-4350, особенно если FMA использовать. Но, мне кажется, это будет не совсем честно по отношению к другим процессорам 😉.
Жесть... увы, но все забили на паскаль :( Как я понимаю, он был достаточно популярен среди научного сообщества в России или все еще популярен?
ОтветитьУдалитьУ PileDriver AVX действительно условный, он как бы есть, но FPU все еще 128 битный. С другой стороны он достаточно старый, чтобы под него тест подгонять :)))
от FMA я у себя существенное ускорение увидел только после оптимизации обращения к кэшу, да и то как я понимаю не столько из-за математики, сколько от экономии регистров. Последняя итерация алгоритма в процессе, ушел от Java в C, чтобы точнее контролировать векторизацию. Пытаюсь выжать все соки из моего процессора! Параллельно экспериментирую с СЛАУ из 3-х уравнений, там тоже все достаточно интересно, как оказалось.
> С другой стороны он достаточно старый, чтобы под него тест подгонять :)))
УдалитьАга. Если бы стояла задача конкретно под него оптимизировать, можно было бы думать... А так, мне кажется, для задачи тестирования вообще не нужно код подгонять под какой-то конкретный процессор, что бы все были в одинаковых условиях.
> от FMA я у себя существенное ускорение увидел только после оптимизации обращения к кэшу,
Это непонятный момент. Я у себя тоже проводил эксперимент, пытаясь увеличить коэффициент использования кэша. Суть заключалась в том, что бы одновременно обрабатывать не две строки во внутреннем цикле, а четыре. Удивительно, но выигрыша не получил, скорее небольшой проигрыш.
Поэтому у меня сложилось впечатление, что ты добиваешься существенного роста на больших размерностях не только из-за более полного использования кэша, но и из-за того, что группируешь вычисления, стараясь их выполнить как можно больше в одной точке памяти. Но тут, возможно, я что-то неправильно понял.
> экспериментирую с СЛАУ из 3-х уравнений,
Интересно посмотреть на твои результаты. Мне пока удалось ускорить в 1.5 раза на FPU. На SSE/AVX пока решил не переносить, так как основная задача - сравнение производительности. Я думаю, относительные цифры практически не изменятся: в 1.5 раза ускорится на одном процессоре, и во столько же - на другом, следовательно, отношение останется тем же.
Но без такой оптимизации на малом размере доля целочисленных операций в коде выше, что может вносить погрешность в измерение производительности с плавающей точкой. Поэтому все же как-нибудь доделаю эту часть.
> не нужно код подгонять под какой-то конкретный процессор, что бы все были в одинаковых условиях.
УдалитьТут еще такой момент: Piledriver вроде не умеет AVX2, а мне очень пригодилась VBROADCASTSD из этого набора. Без нее конечно можно обойтись, но будет не честно по отношению к современным процессорам.
> Удивительно, но выигрыша не получил, скорее небольшой проигрыш.
Как я понимаю Вы вычитали одну строку из 3-х строк сразу? Это не то что нужно, это не изменило ситуацию с памятью. Алгоритм все также загружает всю матрицу в процессор чтобы сделать всего одну операцию, только теперь он еще разрывается между 3-мя строками. Нужно подготовить несколько строк (скажем 3) и потом отнять их все сразу от оставшихся. Эти 3 строки останутся в L1 (или L2), а остальная часть матрицы загрузится в процессор только 1 раз, вместо условно 3-х раз.
> не только из-за более полного использования кэша, но и из-за того, что группируешь вычисления, стараясь их выполнить как можно больше в одной точке памяти
Да, но это все связано. Для того чтобы выполнить много операций над одним участком памяти, нужно больше держать данных в кэше.
Мне очень помогло сначала разобраться с умножением матриц, там абсолютно такие же приемы используются. Мне очень понравилось:
- https://www.cs.utexas.edu/~flame/pubs/GotoTOMS_revision.pdf - статья объясняющая и анализирующая эффективное умножение матриц
- https://github.com/flame/how-to-optimize-gemm/wiki#packing-into-contiguous-memory - пошаговая инструкция по статье с кодом.
В методе Гаусса-Жордан тоже используется умножение матриц, его просто нужно увидеть:
1. Разбиваем матрицу на 4 части крестом:
AABBBB
AABBBB
CCDDDD
CCDDDD
CCDDDD
A - квадратная, а значит число строк в B равно числу столбцов в C. Как именно разбивать нужно тестировать, на больших матрицах у меня оптимум где-то в районе 64.
2. "Забываем" про B и D и делаем прямой проход методом Гаусса-Жордана для прямоугольной матрице AC, только когда строки меняем местами, делаем это целиком захватывая B или D. Также:
- не добиваемся единицы на главной диагонали в A
- вместо зануления элементов ниже главной диагонали, записываем туда коэффициенты умножения.
В итоге в A ниже главной диагонали С[i,j] - коэффициент умножения для строки j чтобы вычесть ее из i, а матрица C будет целиком из таких коэффициентов.
3. По обновленной A рассчитываем B.
4. А теперь самое интересное: D заменяется на D-C*B. И на более менее большой СЛАУ именно это узкое место. Зная как быстро перемножить матрицы поможет ускорить и этот алгоритм.
Классический алгоритм Гаусса-Жордана - это частный случай когда ширина C и высота B - единица.
> Интересно посмотреть на твои результаты.
x87 и Scalar AVX я не тестировал, экспериментировал только с Vector AVX:
1. решение в лоб: каждое из 3-х уравнений в отдельном регистре: где-то 40 млн. СЛАУ/с, основное ускорение что легко менять строки местами, математика тоже ускоряется, но выделение максимума, деление, обратный проход - не векторизуемы, и именно это занимает много времени в совокупности.
2. решение 4-х СЛАУ за раз используя 12-ть 256-бит регистров: 85 млн. СЛАУ/с, весь код векторизован, включая деление и выделение максимума (используя VBLENDVPD для перестановки элементов), ни единого условного перехода.
3. "читерская" структура данных, удобная для предыдущего метода: сначала 4 1-х коэффициента 4-х СЛАУ, дальше 4 2-х коэффициента 4-х СЛАУ и так до 12-го коэффициента, потом 4-ре следующих СЛАУ. В данном случае 100 млн. СЛАУ/с.
На Ryzen 5 думаю будет еще быстрее, так как у него VBLENDVPD в 2 раза быстрее вроде.
Про алгоритм: это только одна итерация, дальше разбиваем обновленную D опять крестом и все повторяем
Удалить> Piledriver вроде не умеет AVX2, а мне очень пригодилась VBROADCASTSD
УдалитьДа, так и есть. Но там вроде два варианта: с регистром и с памятью. Вот с регистром да, из AVX2 набора.
Результаты прямо супер! Особенно впечатляет параллельное решение СЛАУ. Феноменальная скорость получилась!
> ...он был достаточно популярен среди научного сообщества в России или все еще популярен?
УдалитьНе знаю, честно говоря, давно никакого отношения к научному сообществу не имею.
Но в общем-то Pascal изначально задумывался именно как средство обучения. В этом качестве был какое-то время популярен, у нас в России этот период был существенно позже, чем в США.
Но как промышленное средство разработки никогда не имел очень серьезного значения. Там всегда правили в основном С-подобные языки. Корпоративные стандарты, что делать. )
> Зная как быстро перемножить матрицы поможет ускорить и этот алгоритм.
УдалитьТы имеешь в виду алгоритм Винограда — Штрассена или что-то подобное? Прямо интересно стало, точно ли он дает существенное преимущество при размерах матриц порядка нескольких тысяч элементов? Ведь там кроме явного ускорения есть и неявные издержки по рекурсивным операциям умножения матриц.
Но круто, конечно. Ты очень глубоко в тему погрузился.
> в общем-то Pascal изначально задумывался именно как средство обучения.
УдалитьВот никогда не понимал этого разделения на языки для обучения и промышленные. Смысл учить на том что не используется на деле? Синтаксис C подобных языков сложнее? Не уверен, вроде плюс минус тоже самое, если не углубляться в дебри C++ (скажем в шаблоны).
> Корпоративные стандарты, что делать. )
По мне так паскаль ну уж сильно многословен и местами неоправданно строг (тут я в основном про определение переменных в начале функции). Вот честно не было ни единого желания на нем писать после универа :)) Но это так мое личное отношение.
> Ты очень глубоко в тему погрузился.
Это точно, я узнал много нового для себя, не знаю правда пригодятся ли эти знания где-нибудь, но в любом случае - это интересно.
> Ты имеешь в виду алгоритм Винограда — Штрассена или что-то подобное?
УдалитьНет, я про блочный алгоритм, который не изменяет асимптотику, число умножений и сложений остается абсолютно тем же n^3, но выполняет их в более дружественном к памяти порядке. Попробовал расписать здесь: https://gist.github.com/ivankolesnikov/acdf7bfc07d65c449565fdac98623ab5
> Попробовал расписать здесь:
УдалитьОчень хорошо и понятно. Если когда-нибудь руки дойдут, обязательно попробую реализовать.