25.05.2021

UInt128

Решил как-то на досуге накидать тип для удобной работы с длинным беззнаковыми целыми. На плюсах перегрузка операций была изначально, а вот в Object Pascal ее добавили позже, причем с ограничениями. В частности, для перегрузки операций можно использовать только записи (record), но не классы.

В процессе написания, естественно, возникло желание потестить на производительность, пока что только операции сложения/вычитания. Но вот с чем сравнивать результат? Подумал и решил, что можно сравнивать производительность со встроенным 64-битными беззнаковыми целыми (UInt64).
Логика тут такая: Delphi генерирует код в нативных командах процессора, одно сложение - одна команда. А при реализации своего типа с перегруженными операциями взамен нативных команд будет вызов процедуры.
Таким образом, одна команда сложения для UInt64 в случае UInt128 будет заменять на вызов процедуры с 4 обращениями к памяти и двумя командами сложения. Примерно прикинул и подумал, что троекратное снижение производительности в этом случае будет нормально.

Запускаю тест на отладочной версии и получаю следующие результаты: UInt64 на pascal дает примерно 533 млн. сложений в секунду, а UInt128 ‒ 247 млн. Вроде бы не плохой результат: замедление не в 3, а всего лишь в 2.15 раза.
Думаю, а что будет, если переписать UInt64 на assembler? Недолго думая делаю и получаю 3 млрд. 600 млн. регистровых операций в секунуду! Подумать только! 3.6 млрд. 64-разрядных сложений в секунду, Карл! И это старенький, не топовый процессор. Что же сейчас творят топы!?

А я ведь помню еще те времена, когда и сто 16-разрядных операций в секунду были просто мечтой!

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

С чем еще можно померится производительностью? Вспоминаю, что в Java и в .NET есть встроенный тип BigInteger. Он, правда, весьма универсальный, так как имеет "бесконечную" точность, в отличие от того типа, что делаю я, да еще и знаковый. Но мне интересно, сколько стоит такая универсальность?

На Java писать под Windows мне совсем не хочется, даже консольное приложение, но можно вот взять PascalABC.NET, под него и код-то особо переписывать не нужно. Естественно, организую тест так, что бы данные BigInteger занимали больше 8 байт, но меньше 16, что бы сравнение было корректным.
Вот результаты: UInt64 ‒ 477 млн. операций в секунду. Честно говоря, очень достойный результат для .NET, Delphi в отладочной версии дает чуть больше. А вот BigInteger уже не так хорош: в районе 105 млн. операций в секунду. То есть примерно в 2.5 раза хуже, чем мой тип на Delphi.
Думаю, не такая уж большая цена за универсальность. Но иногда производительность все же важнее.

А потом я компилирую на Delphi вместо отладочного варианта релиз и удивляюсь: код для UInt64 на pascal оказывается быстрее кода на assembler ‒ 4 млрд. 179 млн. сложений! Видимо, Delphi все-таки немного умеет в оптимизацию: в цикле я развернул сложения по 10 штук, что бы снизить погрешность измерения от кода, организующего сам цикл.
Видимо, оптимизатор просек эту тему, и заменил 10 команд сложения на одну. Но это не точно: код в релизе без отладочной информации, а копаться в исходных кодах всех прочих библиотек, которые задействованы по умолчанию в работе программы у меня желания нет.
Но хочу заметить, что .NET так не умеет!

Впрочем, сложение ‒ это так себе тема, гораздо интереснее будет, когда я доберусь до умножения, а особенно ‒ до деления.