Пообещав выпустить новый релиз коннектора к тарантулу к концу июля, я не представлял себе полный объём работ. Особенно, учитывая новую работу и прочие изменения в жизни. На сегодня могу сказать, что работы над реализацией нового пакета msgpack.spec завершены. Целью было достичь максимально возможной производительности и эффективности использования памяти: в идеале оно должно быть нулевым. В целом, я считаю, что цель достигнута в общем и целом. Дополнительной памяти никакой не выделяется, ну и при простой упаковке медленнее, чем нативный, всего в два раза. Но в фейсбуке я писал, что в 4. Давайте разберёмся, в чём фокус. Бенчмарк простой: упаковать в msgpack массив из 100 достаточно больших целых чисел через общий метод для запаковки целых чисел. Вызываемый метод обязан проверить все границы и только на последнем шаге вызвать конкретную упаковку.
C# код для бенчмарка:
С код для бенчмарка, с использованием msgpuck:
C++ код для бенчмарка, с использованием msgpack-c:
Результаты бенчмарков ниже. Здесь Empty - это вызов пустого метода для тестирования стоимости PInvoke. То есть нативные бенчмарки выполняются примерно за 82-84нс.
Method | Mean | Error | StdDev | Q3 | Scaled | ScaledSD | Allocated | --------------------- |----------:|----------:|----------:|----------:|-------:|---------:|----------:| MsgPackSpecArrayMinus | 342.31 ns | 3.2070 ns | 2.8429 ns | 343.53 ns | 3.62 | 0.11 | 0 B |
CArrayMinus | 92.95 ns | 1.1212 ns | 0.9363 ns | 93.77 ns | 0.98 | 0.03 | 0 B |
Empty | 10.15 ns | 0.2168 ns | 0.2028 ns | 10.33 ns | 0.11 | 0.00 | 0 B |
CppArrayMinus | 94.17 ns | 2.0049 ns | 3.0009 ns | 95.35 ns | 1.00 | 0.04 | 0 B |
Если честно, я был разочарован. Терять в два раза - ещё ладно, хотя хотелось бы вообще не терять. @EgorBo сказал, что наверно это стоимость Span<T> и метода Slice, которые всё-таки не бесплатные. И, мол, если переписать на пойнтерах в дотнете, то будет также быстро. Ну, хорошо, давайте перепишем на пойнтерах:
Результаты многообещающие, всего на 25% (80 нс - нативный, а у нас 100 нс) хуже, чем нативный код. Что объясняется тем, что у нас есть проверка границ массива на взятии адреса и закрепление массива в памяти, чтобы его GC никуда не двигал при дефрагментации кучи. Не то, чтобы она должна случиться в нашем случае, но так как ГЦ и рантайм ничего не знают о нашем коде в целом, то рантайм закрепит массив в любом случае.
Method | Mean | Error | StdDev | Q3 | Scaled | ScaledSD | Allocated | --------------------- |----------:|----------:|----------:|----------:|-------:|---------:|----------:| MsgPackSpecArrayMinus | 342.31 ns | 3.2070 ns | 2.8429 ns | 343.53 ns | 3.62 | 0.11 | 0 B |
Pointer | 99.37 ns | 1.1726 ns | 1.0969 ns | 100.32 ns | 1.05 | 0.03 | 0 B |
Тут я заметил, что в коде с указателями мы пишем числа как есть, а процессор у меня Intel, т.е. порядок байт - little endian. А в msgpack - порядок байт big endian. Так может быть разворачивание байт такое медленное в дотнете?
Method | Mean | Error | StdDev | Q3 | Scaled | ScaledSD | Allocated | --------------------- |----------:|----------:|----------:|----------:|-------:|---------:|----------:| MsgPackSpecArrayMinus | 342.31 ns | 3.2070 ns | 2.8429 ns | 343.53 ns | 3.62 | 0.11 | 0 B |
PointerBigEndian | 103.97 ns | 1.0718 ns | 1.0026 ns | 104.85 ns | 1.13 | 0.03 | 0 B |
Разница незначительная, следовательно переходим к следующей гипотезе - Span<T> дорогой. Берём код с указателями, меняем указатели на спаны и получаем:
Method | Mean | Error | StdDev | Q3 | Scaled | ScaledSD | Allocated | --------------------- |----------:|----------:|----------:|----------:|-------:|---------:|----------:| MsgPackSpecArrayMinus | 342.31 ns | 3.2070 ns | 2.8429 ns | 343.53 ns | 3.62 | 0.11 | 0 B |
SpanBigEndian | 155.05 ns | 2.3034 ns | 2.1546 ns | 157.00 ns | 1.64 | 0.05 | 0 B | SpanLengthBigEndian | 153.94 ns | 1.6384 ns | 1.5326 ns | 155.42 ns | 1.63 | 0.05 | 0 B |
PointerBigEndian | 103.97 ns | 1.0718 ns | 1.0026 ns | 104.85 ns | 1.13 | 0.03 | 0 B |
Ну, безусловно недешёвый. По сравнению с нативным кодом замедление - в два раза. В полтора по сравнению с кодом на указателях. Но всё-таки не в четыре, как в базовом тесте. Указывание длины немного помогает, надо будет поставить потом во все методы в библиотеки. Давайте вернём упаковывание чисел через высокоуровневый BinaryPrimitives
класс. Ну, в нашем случае высокоуровневым.
Method | Mean | Error | StdDev | Q3 | Scaled | ScaledSD | Allocated | ----------------------------- |----------:|----------:|----------:|----------:|-------:|---------:|----------:|
MsgPackSpecArrayMinus | 342.31 ns | 3.2070 ns | 2.8429 ns | 343.53 ns | 3.62 | 0.11 | 0 B |
SpanLengthBigEndian | 153.94 ns | 1.6384 ns | 1.5326 ns | 155.42 ns | 1.63 | 0.05 | 0 B | SpanBigEndianBinaryPrimitive | 162.62 ns | 2.3434 ns | 2.1920 ns | 164.92 ns | 1.72 | 0.06 | 0 B |
Да, высокий уровень и несколько дополнительных проверок стоили нам ещё около 10 нс. Но всё ещё не 200 нс. И тут я заметил то, что в бенчмарке с указателями я сериализую basiInt
, а в начальном - baseInt - i
. Может ли из-за простого вычитания так просесть перфоманс? Давайте проверим.
Method | Mean | Error | StdDev | Q3 | Scaled | ScaledSD | Allocated | --------------------- |----------:|----------:|----------:|----------:|-------:|---------:|----------:| MsgPackSpecArrayMinus | 342.31 ns | 3.2070 ns | 2.8429 ns | 343.53 ns | 3.62 | 0.11 | 0 B |
MsgPackSpecArray | 164.12 ns | 3.2278 ns | 3.9640 ns | 166.59 ns | 1.74 | 0.07 | 0 B |
Вуаля. А может вычитание и в нативном коде нам даст такой эффект?
Method | Mean | Error | StdDev | Q3 | Scaled | ScaledSD | Allocated | ----------------------------- |----------:|----------:|----------:|----------:|-------:|---------:|----------:|
MsgPackSpecArrayMinus | 342.31 ns | 3.2070 ns | 2.8429 ns | 343.53 ns | 3.62 | 0.11 | 0 B |
MsgPackSpecArray | 164.12 ns | 3.2278 ns | 3.9640 ns | 166.59 ns | 1.74 | 0.07 | 0 B |
CArray | 94.60 ns | 1.9332 ns | 2.9523 ns | 96.06 ns | 1.00 | 0.00 | 0 B |
CArrayMinus | 92.95 ns | 1.1212 ns | 0.9363 ns | 93.77 ns | 0.98 | 0.03 | 0 B |
CppArray | 84.76 ns | 1.3228 ns | 1.2374 ns | 85.66 ns | 0.90 | 0.03 | 0 B |
CppArrayMinus | 94.17 ns | 2.0049 ns | 3.0009 ns | 95.35 ns | 1.00 | 0.04 | 0 B |
Непохоже. В общем, я так и не понял, что происходит и зафайлил баг в .net core. Ну, а без этого - всё прекрасно. Всего лишь в два раза медленнее, чем нативный код. Будем надеяться, что рантайм ускорят, а баг с минусом найдут и поправят :).
comments powered by Disqus