Производительность
Сколько времени занимает прогон и сколько памяти он просит. Каждое число здесь измерено на одной машине запуском пяти опубликованных командных строк на одном и том же конфиге — не прикинуто и не вспомнено из прошлой версии.
Короткий ответ, если больше ничего не нужно: два миллиона записей по шесть полей занимают от девяти до пятнадцати секунд, а на потоковом движке память не растёт с числом строк. 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 000 | 0,7 МБ |
| средний | 200 000 | 14 МБ |
| большой | 2 000 000 | 141 МБ |
Около 74 байт на строку, так что любой размер считается отсюда: гигабайт — это примерно четырнадцать миллионов строк такой формы.
Время
Секунды, лучшее из трёх прогонов (из двух на самом большом размере). Меньше — лучше.
| 10 000 строк 0,7 МБ | 200 000 строк 14 МБ | 2 000 000 строк 141 МБ | |
|---|---|---|---|
| Rust | 0,05 / 0,04 | 0,87 / 0,89 | 8,97 / 8,82 |
| Java | 0,30 / 0,29 | 1,21 / 1,19 | 9,62 / 9,50 |
| Node.js | 0,22 / 0,23 | 1,21 / 1,41 | 12,97 / 14,37 |
| C# | 0,30 / 0,29 | 1,78 / 1,76 | 14,37 / 15,34 |
| Python | 0,55 / 0,66 | 8,35 / 10,24 | 91,30 / 112,11 |
В каждой ячейке — движок 1 / движок 2. Тот же прогон на самом большом размере, со столбиками:
| 2 000 000 строк141 МБ | движок 1 — в памяти | движок 2 — потоковый |
|---|---|---|
| Rustcrates.io | ||
| JavaMaven Central | ||
| Node.jsnpm | ||
| C#NuGet | ||
| PythonPyPI |
На десяти тысячах строк вы меряете в основном запуск: как поднимается JVM, как Python импортирует модули. Примерно до ста тысяч строк выбор реализации почти не важен.
Память
Пиковая занятая память, мегабайты. Меньше — лучше.
| 10 000 строк 0,7 МБ | 200 000 строк 14 МБ | 2 000 000 строк 141 МБ | |
|---|---|---|---|
| Rust | 10,6 / 3,7 | 146 / 3,7 | 1322 / 3,7 |
| C# | 53,6 / 48,5 | 187 / 49,4 | 1375 / 49,4 |
| Python | 40,0 / 32,1 | 197 / 32,2 | 1529 / 32,3 |
| Node.js | 97,6 / 98,0 | 190 / 154 | 1188 / 190 |
| Java | 147 / 120 | 885 / 395 | 4140 / 397 |
| 2 000 000 строк141 МБ | движок 1 — в памяти | движок 2 — потоковый |
|---|---|---|
| Node.jsnpm | ||
| Rustcrates.io | ||
| C#NuGet | ||
| PythonPyPI | ||
| JavaMaven Central |
Если читать только одну таблицу, то эту. На движке 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 Central | ||
| Rustcrates.io | ||
| Node.jsnpm | ||
| C#NuGet | ||
| PythonPyPI |
| 200 000 строк4,1 МБ | движок 1 — в памяти | движок 3 — точный на диске |
|---|---|---|
| C#NuGet | ||
| PythonPyPI | ||
| Rustcrates.io | ||
| Node.jsnpm | ||
| JavaMaven Central |
Движок 3 здесь медленнее и легче — ровно та сделка, ради которой он есть. Его цена к тому же
растёт быстрее, чем у движка 1, с ростом числа строк: он проверяет внешней сортировкой. Поэтому
очень большой прогон с uniq — единственный случай на этой странице, где стоит померить свой
конфиг, а не читать таблицу.
На конфиге с uniq движки 1 и 3 расставляют значения по-разному. Оба результата верны, оба
точно воспроизводимы, и внутри каждого движка все пять реализаций сходятся побайтово. Различие
между движками в том, что к уникальности они идут разными путями.
На практике это не может застать врасплох: движок берётся из конфига, поэтому один конфиг всегда получает один движок и, значит, один ответ. Увидеть разницу можно, только навязав движок вручную, — и это же причина не делать этого без нужды.
Как повторить у себя
Прогонщик лежит в репозитории и сам ставит опубликованные командные строки:
python3 bench/cli_bench.py --config customers --tier all --repeats 3
=== 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
Хеш в конце каждой строки — самое важное: он одинаков у всех пяти, поэтому времена сравнимы — работа была одна и та же.
Смотрите также
- Большие выводы — движки целиком и как удержать большой прогон в рамках памяти.
- Уникальные значения — что обещает
uniqи чего это стоит. - Справочник командной строки —
--jobs,--engine,--now.