Перейти к основному содержимому

Производительность

Сколько времени занимает прогон и сколько памяти он просит. Каждое число здесь измерено на одной машине запуском пяти опубликованных командных строк на одном и том же конфиге — не прикинуто и не вспомнено из прошлой версии.

Короткий ответ, если больше ничего не нужно: два миллиона записей по шесть полей занимают от девяти до пятнадцати секунд, а на потоковом движке память не растёт с числом строк. Python выбивается — около девяноста секунд.

Что и как мерили

Замер гоняет ту командную строку, которую выдаёт реестр, установленную в отдельную папку. Ничего из репозитория не читается — это же и позволяет повторить замер человеку со стороны.

Каждое правило ниже существует, чтобы это осталось измерением, а не рекламой:

  • Один и тот же файл конфига, байт в байт; подставляются только count и engine.
  • Движок выбирается в конфиге, а не флагом — так его точно понимают все пять.
  • --now прибит, иначе генератор дат уплывёт между прогонами.
  • --jobs 1 везде. По-настоящему распараллеливается только TypeScript; мерить эту возможность против четырёх однопоточных — значит мерить не движок.
  • Время и память снимает /usr/bin/time -l снаружи процесса, чтобы никто не измерял себя сам.
  • Каждый вывод хешируется, и хеши обязаны совпасть. Число скорости для прогона, выдавшего другие данные, бесполезно, поэтому расхождение роняет строку, а не попадает в отчёт. Все числа ниже — с прогонов, совпавших побайтово.

Машина

ПроцессорApple M2 Max, 12 ядер (8 производительных, 4 экономичных)
Память32 ГБ
НакопительApple SSD AP1024Z, 1 ТБ, APFS, TRIM включён — заполнен на 94%
СистемаmacOS 26.5.1

Накопитель влияет меньше, чем кажется, и это стоит показать, а не заявить. Последовательная запись на этом томе — 810 МБ/с на холодную и около 1,3 ГБ/с на прогретую. Самый большой прогон здесь порождает файл на 141 МБ, и записать 141 МБ вместе со сбросом на диск занимает 0,24 секунды по прямому замеру — из прогона в девять–пятнадцать секунд это два-три процента, а у Python четверть процента. Эти числа упираются в процессор, поэтому переносите их на свою машину по мощности ядра, а про диск можно почти не думать. Ничего большого не читается: паки весят килобайты и кешируются после первого обращения.

Версии

Все числа сняты на опубликованной 0.1.4 ровно в том виде, в каком её ставит пользователь, с одним исключением: Rust собирался из исходников, потому что его выпущенный потоковый движок ещё держал весь прогон при записи в файл. Это исправление вышло в 0.1.5, так что числа Rust — это 0.1.5, а остальные четыре — 0.1.4.

Между двумя выпусками в движке поменялись маршрутизация при отказе и три диагностических сообщения — ничто из этого не влияет ни на скорость строки, ни на объём памяти. Числа остаются верны для 0.1.5.

Три движка, коротко

Выбирать ничего не нужно: TDC берёт движок из вашего конфига, детерминированно, и один и тот же конфиг получает один и тот же движок на любой машине. Таблицы разделены по движкам только потому, что это объясняет форму чисел.

ДвижокЧто делаетЧем платит
1 — в памятиДержит колонки целиком и отвечает мгновенноПамять растёт с числом строк
2 — потоковыйСчитает по одной строкеПамять не растёт; здесь идёт почти всё
3 — точный на дискеДержит обещания о готовой колонке, например uniqПамять ограничена, платит внешней сортировкой

Разница между 2 и 3 — та, которую стоит запомнить, и она не в степени. Уникальность — это обещание о законченном наборе данных, а не об одной строке, поэтому её нельзя решить построчно: потоковому движку пришлось бы знать, что будет дальше. Поэтому вторая таблица ниже сравнивает движок 1 с движком 3: движок 2 для того конфига не кандидат вообще.

Полный разбор, включая пять форм конфига, которые возвращают прогон на движок 1, — в Больших выводах.

Обычный конфиг

Шесть полей, подобранных так, чтобы стоить разного, а не выглядеть правдоподобно: счётчик, две взвешенные выборки из пака данных, точное процентное деление, число с дробной частью, дата и значение, собранное из двух других.

<sequence name="Id"><gen type="increment" value="1"/></sequence>
<sequence name="First"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="Last"><gen type="template" value="person.lastName"/></sequence>
<sequence name="Status"><gen type="text" value="active,trial,closed" percent="70,20,10"/></sequence>
<sequence name="Balance"><gen type="number" value="0..99999" decimals="2"/></sequence>
<sequence name="Joined"><gen type="date" range="2015-01-01..2025-12-31" format="YYYY-MM-DD"/></sequence>

Три размера ниже стоит просто почувствовать: файл, который откроешь в редакторе, файл, который не откроешь, и файл, который копируется не мгновенно:

СтрокCSV на выходе
маленький10 0000,7 МБ
средний200 00014 МБ
большой2 000 000141 МБ

Около 74 байт на строку, так что любой размер считается отсюда: гигабайт — это примерно четырнадцать миллионов строк такой формы.

Время

Секунды, лучшее из трёх прогонов (из двух на самом большом размере). Меньше — лучше.

10 000 строк
0,7 МБ
200 000 строк
14 МБ
2 000 000 строк
141 МБ
Rust0,05 / 0,040,87 / 0,898,97 / 8,82
Java0,30 / 0,291,21 / 1,199,62 / 9,50
Node.js0,22 / 0,231,21 / 1,4112,97 / 14,37
C#0,30 / 0,291,78 / 1,7614,37 / 15,34
Python0,55 / 0,668,35 / 10,2491,30 / 112,11

В каждой ячейке — движок 1 / движок 2. Тот же прогон на самом большом размере, со столбиками:

2 000 000 строк141 МБдвижок 1 — в памятидвижок 2 — потоковый
Rustcrates.io8,97с8,82с
JavaMaven Central9,62с9,50с
Node.jsnpm12,97с14,37с
C#NuGet14,37с15,34с
PythonPyPI91,30с112,11с
Секунды на файл в 141 МБ. У обоих столбцов одна шкала, поэтому столбики сравнимы по всей таблице; зелёный — самый быстрый замер в ней, красный — самый медленный.

На десяти тысячах строк вы меряете в основном запуск: как поднимается JVM, как Python импортирует модули. Примерно до ста тысяч строк выбор реализации почти не важен.

Память

Пиковая занятая память, мегабайты. Меньше — лучше.

10 000 строк
0,7 МБ
200 000 строк
14 МБ
2 000 000 строк
141 МБ
Rust10,6 / 3,7146 / 3,71322 / 3,7
C#53,6 / 48,5187 / 49,41375 / 49,4
Python40,0 / 32,1197 / 32,21529 / 32,3
Node.js97,6 / 98,0190 / 1541188 / 190
Java147 / 120885 / 3954140 / 397
2 000 000 строк141 МБдвижок 1 — в памятидвижок 2 — потоковый
Node.jsnpm1188МБ190МБ
Rustcrates.io1322МБ3,7МБ
C#NuGet1375МБ49,4МБ
PythonPyPI1529МБ32,3МБ
JavaMaven Central4140МБ397МБ
Пиковая память на том же файле в 141 МБ, на одной шкале. Правый столбец — это и есть то, ради чего потоковый движок существует: 3,7 МБ у Rust против его же 1322 МБ слева.

Если читать только одну таблицу, то эту. На движке 1 память идёт следом за числом строк: в десять раз больше строк — примерно в десять раз больше памяти, в любой реализации. На движке 2 она не двигается вообще: Rust держит 3,7 МБ хоть на десяти тысячах строк, хоть на двух миллионах, C# около 49 МБ, Python около 32 МБ.

Вот что предлагает потоковый движок, и это не «быстрее». Иногда он даже чуть медленнее. За эти доли секунды вы покупаете прогон, память которого можно предсказать до запуска.

Конфиг с uniq

Сочетания, не повторяющиеся нигде в прогоне: 150 × 150 × 150 возможностей, и 200 000 строк занимают около шести процентов пространства.

<sequence name="Pair" uniq="true">
<gen type="text" name="City" value="C000,C001,…,C149"/>
<gen type="text" name="Grade" value="G000,G001,…,G149"/>
<gen type="text" name="Slot" value="S000,S001,…,S149"/>
</sequence>

Файл меньше, чем выше: три коротких кода в строке вместо шести полей.

200 000 строк4,1 МБдвижок 1 — в памятидвижок 3 — точный на диске
JavaMaven Central0,96с1,32с
Rustcrates.io1,03с1,22с
Node.jsnpm1,26с1,91с
C#NuGet1,35с1,60с
PythonPyPI4,79с8,23с
Секунды. Движка 2 здесь нет, потому что такой конфиг он не берёт вовсе.
200 000 строк4,1 МБдвижок 1 — в памятидвижок 3 — точный на диске
C#NuGet188МБ113МБ
PythonPyPI209МБ76,0МБ
Rustcrates.io216МБ138МБ
Node.jsnpm264МБ204МБ
JavaMaven Central793МБ637МБ
Пиковая память того же прогона. На движке 3 часть памяти возвращает каждая реализация, а две, которые возвращают больше всех, оказываются легче всего, что есть слева.

Движок 3 здесь медленнее и легче — ровно та сделка, ради которой он есть. Его цена к тому же растёт быстрее, чем у движка 1, с ростом числа строк: он проверяет внешней сортировкой. Поэтому очень большой прогон с uniq — единственный случай на этой странице, где стоит померить свой конфиг, а не читать таблицу.

Два движка дают разные данные — и это безопасно

На конфиге с uniq движки 1 и 3 расставляют значения по-разному. Оба результата верны, оба точно воспроизводимы, и внутри каждого движка все пять реализаций сходятся побайтово. Различие между движками в том, что к уникальности они идут разными путями.

На практике это не может застать врасплох: движок берётся из конфига, поэтому один конфиг всегда получает один движок и, значит, один ответ. Увидеть разницу можно, только навязав движок вручную, — и это же причина не делать этого без нужды.

Как повторить у себя

Прогонщик лежит в репозитории и сам ставит опубликованные командные строки:

python3 bench/cli_bench.py --config customers --tier all --repeats 3
python3 bench/cli_bench.py --config customers --tier medium
=== customers medium: 200 000 rows
npm        e1     1.21s     189.9 MB  3bca9c07410bf117
pypi       e1     8.35s     196.9 MB  3bca9c07410bf117
crates.io  e1     0.87s     146.1 MB  3bca9c07410bf117
nuget      e1     1.78s     187.0 MB  3bca9c07410bf117
maven      e1     1.21s     885.2 MB  3bca9c07410bf117

every implementation produced identical bytes, on every engine it ran

Хеш в конце каждой строки — самое важное: он одинаков у всех пяти, поэтому времена сравнимы — работа была одна и та же.

Смотрите также