Я одержим оптимизацией производительности и максимальным повышением эффективности программ. За многие годы я сталкивался с конкретными случаями и распространёнными паттернами, замедляющими работу ПО или компьютеров. В этом посте я расскажу о некоторых из них.

Я назвал пост Surprisingly Slow потому, что замедление было для меня неожиданным, или неоптимальные практики, ведущие к замедлению, настолько распространены, что многие программисты будут удивлены их существованию.

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

Распознавание среды в системах сборки (например, configure и cmake)


Именно эта тема вдохновила меня на создание поста.

Системы сборки перед этапом сборки часто имеют этап распознавания среды / конфигурирования. В мире UNIX преобладают сгенерированные autoconf скрипты configure. Также популярен CMake. Эти инструменты запускают код для проверки состояния текущей системы, чтобы конфигурация сборки подходила для текущей среды сборки. Например, они проверяют, какой компилятор использовать, его версию, баги и возможности.

Такое распознавание среды и конфигурирование являются необходимым злом, потому что машины и среды часто сильно различаются и эти различия нужно учитывать.

Проблема в том, что этот этап конфигурирования часто занимает больше времени, чем сама сборка! Системы сборки в случае мелких программ или библиотек часто тратят по десять с лишним секунд на выполнение configure, а сама компиляция и компоновка выполняются за малую долю от этого времени. Другими словами, подготовка к сборке занимает больше времени, чем сама сборка!

Заметность этого расхождения зависит от количества ядер ЦП. Но в моей основной машине стоит 16-ядерный/32-потоковый Ryzen 5950X, и мне мучительно наблюдать за относительной медленностью этапа конфигурирования.

Но ещё более шокирующим мне кажется то, что время конфигурирования часто намного превышает время сборки даже для крупных проектов. Не знаю, справедливо ли это сегодня, но несколько лет назад Mozilla заметила, что сборка LLVM/Clang на инстансе с 96 vCPU EC2 требовала больше времени на cmake/конфигурирование, чем на компиляцию и компоновку! А ведь это очень крупный проект на C++ с тысячами файлов исходного кода!

Конфигурирование сборки часто является отдельным этапом, выполняемым последовательно перед тем, что большинство людей считает настоящей сборкой. Для повышения эффективности конфигурирование сборки необходимо распараллелить. Ещё лучше было бы, если бы она интегрировалась в сам основной DAG сборки, чтобы можно было начать выполнять части сборки без необходимости ожидания конфигурирования сборки. К сожалению, многие популярные инструменты конфигурирования сборки нельзя с лёгкостью адаптировать к этой модели. Поэтому многие из нас почти ничего не могут с этим поделать.

Ещё одно решение этой проблемы — полное устранение проблемы распознавания среды. Если вы работаете в детерминированных и воспроизводимых средах сборки, то можно срезать углы, чтобы пропустить распознавание среды, которое вам больше не нужно. Примерно такой подход используют современные инструменты сборки наподобие Bazel. Мне любопытно, насколько ускоряется сборка в инструментах типа Bazel благодаря устранению этапа конфигурирования среды. Подозреваю, что сильно!

Лишнее время на новый процесс в Windows


В Windows новые процессы не могут создаваться так же быстро, как в операционных системах на основе POSIX, например в Linux. Стоит ожидать, что в Windows создание нового процесса займёт 10-30 мс. В Linux создание новых процессов (часто посредством fork() + exec()) занимает максимум единицы миллисекунд.

Однако создание потоков в Windows выполняется очень быстро (порядка десятков микросекунд).

Подробнее об этом можно прочитать в темах на Stack Overflow: первая и вторая.

Несколько десятков миллисекунд в контексте ЦП — целая бесконечность. И это достаточно много для задач, которые воспринимаются людьми как мгновенные. Возможно, в том числе из-за этого кажется, что Windows медленнее Linux.

Если архитектура вашей программы состоит из постоянного создания новых процессов (что часто встречается в мире UNIX), то в Windows это может создавать проблемы с производительностью, так как лишнее время на создание нового процесса в Windows может сильно масштабироваться:

  • 10 мс * 1000 вызовов = 10 с
  • 20 мс * 10000 вызовов = 200 с
  • 30 мс * 100000 вызовов = 3000 с

Возьмём для примера файлы configure из предыдущего раздела поста, которые часто бывают скриптами оболочки. А скрипты оболочки часто выполняют свою работу, создавая другие процессы, например, grep, sed и sort. Даже оператор [ может быть новым процессом (серьёзно: в вашем POSIX-окружении, вероятно, есть исполняемый файл /usr/bin/[). (Хотя [ может быть встроен в оболочку.) Цепочки конвейеров команд (например, command | grep | awk) последовательно создают несколько процессов и их выполнение может казаться медленным. Cкрипт конфигурации может создавать тысячи новых процессов. Если предположить, что на каждый тратится по 10 мс, то при 1000 вызовов лишь на новые процессы будет потрачено 10 с лишнего времени! Это усугубляет проблему, описанную в предыдущем разделе!

Если ваше ПО работает в Windows, то оцените эффект, который оказывает относительно медленное создание процессов. Подумайте над альтернативами: многопоточной архитектурой и использованием долгоживущих демонов/фоновых процессов.

Закрытие дескрипторов файлов в Windows


Много лет назад я профилировал Mercurial, чтобы повысить скорость контрольной проверки папок в Windows, потому что пользователи замечали, что время проверки в Windows было гораздо выше, чем в Linux, даже на одной и той же машине.

Я думал, что можно свести это к разнице между файловыми системами NTFS и Linux или эффективности на общем уровне ядра/ОС. Но на самом деле я выяснил нечто гораздо более удивительное.

Когда я начал профилировать Mercurial в Windows, то заметил, что большинство API ввода-вывода выполняет свою работу за несколько десятков микросекунд, иногда за одну-две миллисекунды. Производительность Windows/NTFS казалась отличной!

За исключением CloseHandle(). Для выполнения этих вызовов часто требовалось 1-10 и более миллисекунд. Мне казалось странным, что запись в файлы (даже непрерывная запись, которой было достаточно для выхода за пределы объёмов любой буферизации) оказывалась быстрой, но закрытие медленным. Ещё больше поражало, что CloseHandle() был медленным, даже при использовании портов завершения (т.е. асинхронного ввода-вывода). Такое поведение портов завершения противоречило тому, что должно происходить по документации MSDN (функция должна мгновенно выполнять возврат, а её состояние можно получать позже).

Хотя тогда я этого не понимал, но причиной такого поведения был/является Windows Defender. Windows Defender (и другое антивирусное/сканирующее ПО) обычно при своей работе в Windows устанавливает нечто под названием «драйвер фильтра файловой системы» (filesystem filter driver). Это драйвер ядра, который, по сути, подключается к ядру и получает обратные вызовы событий ввода-вывода и файловой системы. Оказалось, что обратный вызов закрытия файла запускает сканирование записанных данных. И это сканирование выполняется синхронно, не позволяя CloseHandle() выполнить возврат. Это добавляет миллисекунды лишнего времени. В сумме скорость ввода-вывода изменения файлов в Windows значительно снижается Windows Defender и другими антивирусными сканерами.

Насколько я понимаю, если запущен Windows Defender (и, предположительно, другие антивирусные сканеры), невозможно обеспечить устойчиво высокую скорость API ввода-вывода Windows. Можно отключить антивирусное сканирование (на свой страх и риск). Но Mercurial применяет другой способ (в дальнейшем это эмулируется rustup и другими инструментами) — использует пул потоков для вызова CloseHandle(). Даже если вы выполняете все операции ввода-вывода открытия и записи файлов в одном потоке и используете фоновый пул потоков только для вызова CloseHandle(), то заметите трёхкратное ускорение записи файлов.

В идеале эту оптимизацию должно использовать любое ПО, создающее или изменяющее даже всего несколько сотен файлов в Windows. В список такого ПО входят инструменты контроля версий, установщики и инструменты распаковки архивов. Забавный факт: rustup может распаковывать файлы tar в Windows быстрее, чем опенсорсные и коммерческие быстрые инструменты распаковки/копирования, потому что использует этот и другие трюки. Мне кажется, rustup в Windows на самом деле быстрее распаковывает архивы tar, чем в Linux!

Искусственная задержка ввода-вывода, добавляемая сканирующим ПО наподобие Windows Defender, очень раздражает. Однако рост производительности благодаря обходу этой проблемы при помощи фонового пула потоков часто оправдывает его сложность. Я не сомневаюсь, что будь эта оптимизация включена в популярные инструменты Windows (а именно установщики), то люди были бы поражены, насколько быстро всё может работать.

Запись в терминалы


Как мейнтейнер системы сборки Firefox, я получил десятки отчётов от людей, жалующихся на то, что у них сборки происходят медленнее, чем у коллег на таком же оборудовании. Хотя для этого может быть много причин, одной из самых неожиданных является влияние терминала на скорость сборки.

Запись в терминал обычно выполняется быстро. Но бывают исключения.

Я выяснил, что запись кучи выходных данных или усложнение записи в терминал (например, запись цветов, перемещение позиции курсора для перезаписи предыдущего контента) может значительно замедлить приложения.

Запись в терминал при помощи stderr/stdout с большой вероятностью выполняется с блокированием ввода-вывода. Поэтому если код, управляющий вашим write() (эмулятором терминала) не завершает обработку вовремя, процесс просто ждёт, пока терминал выполнит свою задачу.

Мы выяснили, что разные терминалы обладают собственными особенностями. Исторически, командная строка Windows и встроенное в macOS приложение Terminal.app очень медленно обрабатывали большой объём выводимых данных. Я помню (хотя не могу найти найти этот баг или коммит в Firefox), что когда мы сделали систему сборки «немой» по умолчанию, в некоторых конфигурациях это снизило время сборки на минуты.

Несколько лет назад npm внедрил печально известный снижающий производительность индикатор прогресса. Хотя я не знаю, что было сильнее виновато, медленность терминала или слишком частый вызов кода обновления прогресса, терминал с большой вероятностью сыграл свою роль, потому что у терминалов есть ограничение на частоту принятия входящих данных для отрисовки.

Я обнаружил, что современные терминалы лучше справляются с записью кучи простого текста, чем в 2012 году, когда я решал эти проблемы в системе сборки Firefox. Но я всё равно бы проявлял максимальную осмотрительность со сложными фишками терминала, например, раскраской текста, отрисовкой колонтитулов и т.п. Всегда используйте буферизированный ввод-вывод для минимизации количества происходящих в терминале операций write(), при необходимости выполняя сброс (по возможности в свободное время). Подумайте над использованием асинхронного потока для записи в stdout/stderr. Фиксируйте общее время, потраченное на блокировку ввода-вывода к stdout/stderr, чтобы можно было замерить задержку ввода-вывода терминала. И периодически при запуске программы сравнивайте дельту фактического времени между подключённым к терминалу stdout/stderr и /dev/null, чтобы увидеть, не слишком ли велико различие. Также можно подумать о регулировании записей в терминал. Вместо записи нижнего колонтитула после каждой строки вывода, попробуйте буферизировать строки в течение нескольких миллисекунд и одновременно выводить все линии плюс новый колонтитул. При отрисовке индикатора прогресса, вращающегося индикатора или чего-то подобного я бы ограничивал частоту отрисовки примерно 10 Гц, чтобы минимизировать трату лишнего времени в терминале.

Тепловой троттлинг/состояния ACPI C/P/троттлинг процессора


Мы привыкли думать, что компьютер и его процессоры или включены, или выключены, но если бы всё было так просто…

В процессе работы процессоры постоянно меняют рабочий диапазон. Все приведённые ниже заявления справедливы (хотя и не каждый пункт относится ко всем машинам или моделям ЦП):

  • Количество МГц, на котором работает ядро ЦП, может сильно колебаться каждую секунду.
  • Ядра ЦП могут уходить в сон или в режим с очень низким энергопотреблением, даже если остальные продолжают работать.
  • При превышении порогового значения температуры ядра могут значительно снижать тактовую частоту. Они могут отказываться работать быстрее, прежде чем снизится температура. Неисправные датчики могут приводить к преждевременному срабатыванию защиты.
  • Ядра могут достигать своей максимальной частоты, если работают и другие ядра. Может иметь значение физическая близость других ядер.
  • Для разгона до полной скорости простаивающему ядру могут понадобиться десятки, сотни или даже тысячи миллисекунд.
  • Процесс изменения мощности может очень сильно варьироваться в зависимости от того, подключена ли машина к внешнему источнику питания, или работает от аккумулятора.
  • Изменение мощности может сильно варьироваться от того, заряжен ли аккумулятор полностью или почти разряжен.
  • Ноутбуки Apple могут уходить в тепловой троттлинг, когда заряжаются с левой стороны. (Да, серьёзно: всегда заряжайте свой MacBook Pro справа. А если ваши сотрудники используют ноутбуки Apple для задач, активно занимающих ЦП, то сообщите им о необходимости зарядки справа. Или даже лучше — установите ПО, проверяющее, выполняется ли зарядка слева, и выдающее уведомление. Однако я пока не смог найти ПО или API, способное это распознавать.)
  • Ядро может замедлять работу для обработки определённых команд (например, AVX-512).

Современные ЦП — очень динамичные устройства, и режим их работы часто кажется непредсказуемым. Более того, модели ЦП могут сильно отличаться друг от друга. Например, процессор EPYC или Xeon, скорее всего, будет вести себя иначе, чем Ryzen или Core i7/i9, которые тоже ведут себя по-разному в десктопах и ноутбуках. (Несколько лет назад я заметил, что ядра Xeon не так легко переходят в турбо-режим, как ЦП потребительского уровня.)

Колебания мощности и их влияние на производительность — одна из причин чрезвычайной сложности проведения точных бенчмарков. При выполнении бенчмарков необходимо отслеживать переменную мощности или, по крайней мере, сообщать о его состоянии, чтобы результаты квалифицировались соответствующим образом. Я очень скептично отношусь к результатам бенчмарков, не указывающих конфигурацию мощности и методологию её фиксации (к сожалению, таково большинство бенчмарков), и особенно к бенчмаркам, проводимым на ноутбуках, поскольку работающие от аккумуляторов устройства гораздо сильнее подвержены троттлингу мощности, чем десктопы или серверы.

Лично мой MacBook Pro уходил в тепловой троттлинг, потому что открутился внутренний винт и мешал раскрутке кулера. macOS не предупредила меня: я знал только то, что мои сборки Firefox без каких-то причин стали медленнее в два-три раза! Также я сталкивался с нагревом MacBook Pro из-за зарядки с левой стороны. Зарядка справа волшебным образом ускорила работу.

Когда мы начали выкатывать десктопы на Xeon для сотрудников в Mozilla, мы стали получать отчёты о сильно меняющихся скоростях сборки. В некоторых операционных системах (Mozilla имела очень небрежное централизованное управление машинами, позволяя сотрудникам полностью управлять переданным компанией оборудованием), по умолчанию состояния ACPI C/P были такими, что ядра ЦП масштабировались по-разному.

Мы заметили, что этап компиляции сборки был нормальным. Однако некоторые люди сообщали, что компоновка в 2-4 раза медленнее (от десятков секунд до минут), чем у других на аналогичных конфигурациях! Это стало большой проблемой, потому что фактическое время инкрементной/неполной сборки в основном тратится на компоновку. Со временем мы разобрались, что на медленных машинах занимающееся компоновкой ядро ЦП работало только на 25-50% от своего потенциала, то есть на 1,0-1,5 ГГц. Но если пользователь запускал дополнительные «тяжёлые» нагрузки на ЦП, частота ядра подскакивала. Мы выяснили, что у разных операционных систем используются разные стандартные значения для состояний ACPI C/P. При более консервативных настройках ядра ЦП не масштабируют свою частоту, если только нет достаточной нагрузки на ЦП. Переключение на более агрессивные параметры мощности обеспечило более качественные и стабильные результаты.

Ноутбуки сильно подвержены тепловому троттлингу и агрессивному троттлингу мощности для экономии заряда. Я считаю, что в целом ноутбуки слишком переменчивы, чтобы обеспечивать надёжную производительность. При наличии выбора я бы предпочёл, чтобы тяжёлые нагрузки на ЦП выполнялись в контролируемых и наблюдаемых десктопных или серверных средах.

Но и серверы не защищены полностью от этой проблемы: их параметры состояний ACPI C и P могут значительно влиять на производительность. Можно настроить их на максимум, чтобы все ядра работали на полную мощь (или были готовы к работе на полную через несколько миллисекунд). Однако это может сильно повысить энергопотребление. Это можно сделать у некоторых поставщиков облачных услуг (например, у AWS) без непосредственных затрат для вас. Однако повышение энергопотребления плохо для окружающей среды. Выбросы углекислого газа при использовании дата-центров уже равны объёмам выбросов авиаперевозок (до пандемии), и эти объёмы растут. Поэтому подумайте о своей ответственности, прежде чем настраивать серверы, потенциально увеличивая их мощность на мегаватты.

Запуск Python, Node.js, Ruby и других интерпретаторов


Сложные системы во время своей работы тысячи или более раз выполняют Python, Node.js и другие интерпретаторы. Например, система сборки Firefox вызывает тысячи процессов Python, выполняющих стандартные задачи, например, обёртывание и вызов компилятора. А средства тестирования Mercurial вызывают тысячи процессов Python, запуская по ходу тестирования hg. Я слышал подобные истории о Node.js, Ruby и других интерпретаторах, часто в контексте использования в системах сборки.

При запуске нового процесса интерпретатора игнорируют тот факт, что для инициализации интерпретатора часто требуются миллисекунды или десятки миллисекунд, т.е. новый процесс тратит время в начале выполнения процесса просто на то, чтобы добраться до кода, который вы приказали ему выполнить. Иногда излишние траты ресурсов на новый процесс настолько велики, что торможение заметно и приводит к отказу от технологии. Исторически этим печально известен JVM, и поэтому использование Java обычно приводит к выполнению меньшего количества долгоживущих процессов вместо большего количества процессов с узкой областью действия.

Я уже писал ранее о лишнем времени на запуск Python. В 2014 году я замерил, что средства тестирования Mercurial тратят 10-18% от общего процессорного времени только на то, чтобы добраться до точки, где интерпретатор/процесс сможет выполнить байт-код, а 30-38% от общего процессорного времени — на то, чтобы добраться до точки, где Mercurial выполняет диспетчеризацию команд (дополнительное время здесь в основном тратится на импорт модулей).

Можно подумать, что несколько лишних потраченных миллисекунд не особо важны. Но если умножить это на 1000, 10000, 100000 или более, миллисекунды становятся важными:

  • 1 мс * 1000 вызовов = 1 с
  • 10 мс * 10000 вызовов = 100 с
  • 100 мс * 100000 вызовов = 10000 с (2,77 часа)

В Windows эта проблема усугубляется относительно медленным запуском новых процессов (см. выше).

Программисты должны тщательно продумывать модель вызова процессов. Задумайтесь об использовании меньшего количества процессов и/или другого языка программирования, не тратящих лишних ресурсов, если это может стать проблемой (обычно подходит всё, что компилируется до уровня ассемблера).

Почти весь ввод-вывод накопителя


Особая страсть у меня есть к оптимизации ввода-вывода. Я считаю, что основная причина этого заключается в огромном разрыве между потенциалом современных устройств хранения и тем, что достигнуто на самом деле. Теоретически, ПО может получать примерно в 10 раз бОльшую производительность с современными устройствами хранения, чем мы видим обычно.

Современные устройства хранения до абсурда быстры. Накопитель NVMe в моём основном PC может обеспечивать скорость чтения больше 3 ГБ/с (больше 6 ГБ/с при последовательном чтении), записи около 1 ГБ/с (4 ГБ/с при последовательной записи), способен выполнять больше 500 тысяч операций ввода-вывода в секунду и обслуживать множество операций ввода-вывода в интервале задержки 10 микросекунд. Современные накопители NVMe с точки зрения пропускной способности находятся примерно на одном уровне с производительностью DDR2 DRAM (выпущенной в 2003 году) (задержка чуть больше, но порядок 10 мкс особой роли не играет).

Для сравнения: жёсткий диск Western Digital Caviar Black на 1 ТБ, извлечённый из моего PC несколько недель назад, может выполнять последовательное чтение и запись со скоростью всего 90 МБ/с, произвольное чтение и запись — со скоростью 1-2 МБ/с, и имеет время доступа порядка 12 мс. Не знаю точно, какой у него IOPS, но учитывая время доступа порядка 12 мс и физическую структуру вращающихся дисков, значение не может быть больше нескольких сотен.

Современный накопитель NVMe на 1,5-3 порядка быстрее лучших жёстких дисков, произведённых чуть больше десятка лет назад. Так почему же все операции ввода-вывода с накопителями не выполняются почти мгновенно?

Если вкратце, то большинство ПО не справляется с использованием потенциала современных устройств хранения, или даже хуже того — саботирует их плохими практиками.

О неиспользовании потенциала можно прочитать в превосходной статье Modern Storage is Plenty Fast. It is the APIs That are Bad [перевод на Хабре]. tl;dr статьи: можно воспользоваться полной мощью современного устройства хранения, обойдя стандартные примитивы ввода-вывода ОС/ядра и передавая операции ввода-вывода напрямую устройству. То есть программные абстракции ОС/ядра съедают большую часть потенциала.

Что касается ПО, саботирующего потенциал устройств хранения, то я вкратке расскажу о нём на примере POSIX-функции fsync(). Вызывая эту функцию, вы, по сути, говорите: гарантируй сохранение состояния этого дескриптора файла на устройстве хранения или я не хочу терять любые внесённые изменения.

Целостность и надёжность хранения данных важны. Но цена достижения этой цели может быть абсурдно высокой. И как оказывается, в её правильной реализации на практике есть множество мелких сложностей. Рекомендую прочитать превосходный пост Дэна Луу Files are Hard. Представленные в посте ссылки на статьи отрезвляют. Подкреплю посыл поста статьёй PostgreSQL's fsync() surprise, в которой приведена хроника того, как мейнтейнеры PostgreSQL выясняли, что Linux способен напрямую сбрасывать ошибки при выполнении ввода-вывода с устройством, что ведёт к повреждению данных. Ого!

Но вернёмся к fsync(). Концепция fsync() вполне чёткая: гарантировать то, что этот файл сохранён на устройство хранения. Однако в реализации часто встречается куча неэффективных решений, ведущих к торможению.

Во многих файловых системах Linux (в том числе и ext4) реализация fsync() такова, что при вызовах все несброшенные на накопитель операции записи сохраняются на него. То есть если процесс A выполняет запись из файла на 1 ГБ, а процесс B записывает 1 байт в другой файл и вызывает fsync() для этой операции записи единственного байта, Linux/ext4 обязан записать на устройство хранения 1 ГБ, а не 1 байт. То есть в Linux/ext4 любому процессу достаточно вызвать fsync(), чтобы все элементы кэша грязной страницы обязаны были сброситься на накопитель. В большинстве систем обычно что-то непрерывно вызывает ввод-вывод на запись, поэтому объём ввода-вывода устройства хранения, вызванный fsync(), почти всегда больше, чем объём сохраняемого изменённого файла/каталога.

Такое поведение может вызывать множество проблем. Во-первых, оно искусственно повышает задержку ввода-вывода. Разработчик рассчитывает, что вызов fsync() после внесения минимальных изменений должен выполняться почти мгновенно. Однако если есть множество грязных страниц для сброса, операция может занимать несколько секунд. У моего нынешнего работодателя мы столкнулись именно с этой проблемой в GitHub Enterprise, имеющем монолитную архитектуру. База данных MySQL работала в той же файловой системе ext4, что и репозитории Git. MySQL часто вызывала fsync() для обеспечения транзакций, а журнал транзакций сохранялся на накопитель. Но если был запущен сборщик мусора Git (GC), а Git только что закончил запись многогигабайтного pack-файла, команда fsync() MySQL тормозила, ожидая, пока завершится сохранение на накопитель большого объёма данных Git. Это приводило к замедлению будущих транзакций MySQL и даже к таймаутам на уровне приложения. Когда люди говорят, что базы данных и другие хранилища должны изолироваться в отдельные разделы/файловые системы, одной из причин этого является неуклюжее поведение fsync().

К счастью, более новые версии Linux/ext4 имеют функцию быстрых коммитов, меняющую поведение и обеспечивающую более дробный сброс fsync() на накопитель, как это и написано в документации. Но так как эта функция довольно свежая, для стабилизации и внедрения в дистрибутивы может понадобиться время. А я уже никак не могу этого дождаться!

Ещё одна проблема fsync() заключается в том, что её вызывают гораздо чаще, чем следует. Да, если у вас есть критически важные данные, требующие целостности и надёжности хранения, следует вызывать fsync() тогда, когда необходимо. Но в реальности многие нагрузки обработки данных и машинные среды не требуют абсолютных гарантий сохранности данных!

Для примера можно взять поды Kubernetes или раннеры CI. Или даже серверы для stateless-сервиса. Задайтесь вопросом: что самое плохое может случиться, если отключится питание машины и потеряются данные в локальной файловой системе? В большинстве случаев ответом будет ничего. Вы спроектировали свою систему как stateless и устойчивую к сбоям. Вы управляете серверами как cattle. Вы работаете с локальными файловыми системами как с временными устройствами. Поэтому если машина сбойнёт, вы создадите новую ей на замену. В таких сценариях fsync() почти ничего не даёт вам, но многого стоит!

Затраты на вызовы fsync(), без которых вполне можно обойтись, могут быть значительными. В сочетании с неэффективным поведением глобального сброса на накопитель в Linux/ext4 это может сильно снижать производительность, особенно при медленных устройствах хранения. К счастью, есть и другие варианты. У многих баз данных и другого популярного ПО есть способы обхода вызова fsync(). Если ваши данные временные, то подумайте над тем, чтобы отключить fsync(), скорее всего, вы получите значительный рост производительности! Для ПО, не поддерживающего отключение fsync(), можно использовать инструмент eatmydata и библиотеку LD_PRELOAD, ослабляющие эффект fsync(), а также схожую с ними функциональность, перехватывая вызовы функции и превращая их в no-op. И последнее: для временных машин можно собрать пропатченное ядро Linux, превращающее fsync() и её коллег в no-op. (Не уверен, пользуется ли этим кто-нибудь, но рассматривал такую возможность, потому что внедрение eatmydata, например, в запущенные контейнеры — это мучительный процесс.)

Завершу этот раздел я ссылкой на свой любимый коммит в репозиторий Firefox: Disable Places during reftests, preventing 50 GB of I/O. Хотя этот коммит не только отключает fsync(), вызовы fsync() (и её аналогов в Windows) были виноваты в снижении производительности. Излишний ввод-вывод и ненужное сохранение изменений на устройство могут значительно снижать производительность. ПО накопителей обычно ошибается в сторону целостности (на мой взгляд, это правильное значение по умолчанию). Учитывая затраты, которые накладывает целостность, следует серьёзно поразмыслить над ослаблением гарантий и ускорением ввода-вывода, если этот вариант для вас приемлем.

Сжатие данных


На тему сжатия данных и его повсеместного неоптимального использования я могу написать целый пост. Здесь я приведу краткую версию.

По своей сути, сжатие данных — это компромисс между использованием ЦП и ввода-вывода. Обычно присутствует один из следующих сценариев:

  1. Узким местом является ввод-вывод (с накопителем или сетью), поэтому мы готовы потратить больше ресурсов ЦП для снижения объёмов ввода-вывода.
  2. В состоянии покоя накопитель затратен, поэтому мы готовы потратить больше ЦП для снижения использования/затрат накопителя.

С первых дней развития компьютеров накопители были медленными и дорогими по сравнению с ЦП. Поэтому обмен ресурсов ЦП на экономию использования накопителя казался хорошим компромиссом.

Перенесёмся в 2021 год.

Как я говорил в предыдущем разделе, ввод-вывод современных накопителей до абсурда быстр.

Сети тоже стали быстрее. На текущий момент фактически стандартом стал 1 Гбит/с (125 МБ/с). 2,5 Гбит/с (312 МБ/с) внедряются в пользовательских и офисных средах. 10 Гбит/с (1250 МБ/с) распространены в дата-центрах. И скорости выше 10 Гбит/с уже возможны.

Тем временем, в последнее десятилетие производительность одного ядра ЦП примерно находилась на плато. Мы на несколько лет застряли примерно на 4 ГГц. Весь рост производительности ЦП происходил благодаря добавлению в корпус большего количества ядер ЦП и повышения эффективности выполнения команд за такт (instructions per cycle, IPC) (из-за этой работы с IPC мы также получили ужасающие уязвимости безопасности наподобие Spectre и Meltdown).

Всё это означает, что относительная разница производительностей между ЦП и вводом-выводом сильно уменьшилась. Около 30 лет назад ЦП работал с частотой примерно 100 МГц, а Интернет работал по коммутируемому соединению, скажем 50 кбит/с, или 0,05 Мбит/с, или 6,25 кбод/с. Это составляет 16000 тактов на байт. Сегодня мы имеем примерно 4 ГГц и сети на 1 ГБит/с / 125 МБ/с. Это 32 такта на байт, коэффициент уменьшился в 500 раз. (Если по справедливости, он становится больше, учитывая наличие нескольких ядер ЦП, конкурирующих за ввод-вывод, и улучшение показателя IPC. Но мы всё равно говорим, что относительная разница между ЦП и вводом-выводом уменьшилась на 1-1,5 порядка.) Много лет назад обмен ресурсов ЦП на снижение нагрузки ввода-вывода часто был совершенно верным решением. Сегодня из-за повышения производительности ввода-вывода относительно ЦП и значительно снизившегося соотношения тактов к байту ввода-вывода всё далеко не так однозначно.

Не способствует ясности и преобладание древних алгоритмов сжатия. Алгоритм DEFLATE, используемый в вездесущей библиотеке zlib и формате данных gzip, был придуман примерно 30 лет назад. DEFLATE был спроектирован в эпоху, когда у компьютеров был 1 МБ ОЗУ и жёсткие диски на 100 МБ. В другие времена.

DEFLATE/zlib стали очень популярными в мире, где ввод-вывод был гораздо медленнее, а сжатие часто являлось необходимостью. Если не использовать сжатие при подключении через модем, то разница в производительности будет существенной! А из-за их популярности в первые дни Интернета, DEFLATE/zlib имеются в стандартной библиотеке многих языков программирования. Похоже, это первый формат сжатия, который используют люди, когда кто-то решает добавить сжатие.

Вездесущесть zlib хороша с точки зрения зависимостей: читать zlib/gzip могут все. Однако в случаях, когда ты контролируешь и считывающее, и записывающее устройство, использование zlib в 2021 году является халатностью, потому что его производительность отстаёт от современных решений. Современные библиотеки сжатия (мой фаворит — zstandard) могут обеспечивать значительно более высокие скорости сжатия и распаковки, имея более высокие показатели сжатия с большинством наборов данных. Подробности есть в моей статье 2017 года Better Compression with Zstandard. (Я подумывал вернуться к этому посту, потому что в последующих релизах zstandard было внедрено множество ускорений на 10 и более процентов, из-за чего он становится ещё привлекательнее.) Если вам не нужна вездесущесть zlib (например, вы контролируете и чтение, и запись), нет почти никаких причин выбирать zlib вместо чего-то более современного. По сравнению со zlib современные библиотеки сжатия наподобие zstandard ближе всего к волшебной палочке, которой можно прикоснуться к своему ПО, чтобы обеспечить бесплатный рост производительности.

Если вы используете компрессию (особенно zlib) для сжатия в реальном времени (отправки сжатых данных куда-нибудь, где они мгновенно будут распакованы), вам необходимо измерить линейную скорость систем сжатия и распаковки. А затем сравнить это с линейной скоростью передачи несжатых данных. Становится ли ввод-вывод узким местом в случае несжатой передачи? Если нет, требуются ли вам экономия полосы пропускания или ресурса ввода-вывода при помощи сжатия? Если нет, то зачем вообще использовать сжатие? Вы только что выяснили, что сжатие только искусственно замедляет ваше ПО без всяких причин! Учитывая то, что сжатию zlib часто не удаётся заполнить полностью канал в 1 Гбит/с, существует очень высокая вероятность того, что использование сжатия добавляет искусственное «бутылочное горлышко» на стороне ЦП!

Если вы используете сжатие (особенно zlib) для архивирования данных (хранения где-нибудь сжатых данных, где они время от времени будут распаковываться), вам необходимо измерить и сравнить коэффициенты сжатия и линейные скорости различных форматов сжатия и их параметров. Как и в ситуации со сжатием в реальном времени, если распаковка снижает линейную скорость по сравнению с несжатыми данными, то вы искусственно замедляете доступ к данным. Возможно, это оправданно экономией на объёме накопителей. Однако во многих случаях можно перейти на другую библиотеку сжатия и получить схожие или даже лучшие коэффициенты сжатия, достигнув при этом повышенных скоростей сжатия/распаковки. Кому не захочется бесплатно получить рост производительности и снижение затрат на накопители?

Одной из причин моей любви к zstandard стало то, что его можно настраивать в интервале от чего-то невероятно быстрого (скорости сжатия и распаковки в гигабайтах в секунду), до чего-то очень медленного по сжатию, но достигающего потрясающих коэффициентов сжатия с сохранением скоростей распаковки в гигабайтах в секунду. Это позволяет использовать один и тот же формат для совершенно разных случаев применения. Также можно динамически менять характеристики хранения данных. Например, изначально можно записывать данные с быстрой настройкой, чтобы устройство записи не было ограничено ЦП. А затем можно пакетно пересжимать данные с более агрессивными настройками, сильно уменьшая их в размере. Это совсем непохоже на zlib, интервал параметров сжатия которого находится в пределах от довольно медленного и не очень хороших коэффициентов сжатия до очень медленного и всё равно с не очень хорошими коэффициентами сжатия.

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

  • Установка пакетов Apt (пакеты сжаты gzip). (Забавный факт: установка пакетов apt тоже подвержена описанному выше замедлению fsync(), потому что менеджер пакетов вызывает fsync() не менее одного раза для каждого пакета.)
  • Установка пакетов Homebrew (пакеты сжаты gzip).
  • Установка пакетов Python через pip (архивы исходников — это tarball-ы gzip, а wheels — это файлы zip со сжатием zlib).
  • Push/pull образов Docker (слои внутри образов Docker сжаты gzip).
  • Git (обмен данными по wire protocol и сохранение данных на диск используют zlib). (Когда я добавил в Mercurial поддержку zstandard, это снизило объём передаваемых данных от серверов на 89%, а использование ЦП на стороне серверов снизилось до 60%.)

В корпоративном мире существуют петабайтные хранилища данных, озёра данных, колизеи данных (честно говоря, отстал от жизни и не знаю, как они называются сейчас), хранящие данные в gzip. Вероятно, можно было бы сэкономить десятки терабайт, перейдя на что-нибудь вроде zstandard. При использовании LZMA (имеющего чрезвычайно медленные скорости распаковки) малы затраты на хранение, однако доступ к данным чрезвычайно медленен, из-за чего замедляется выполнение запросов данных. У меня не было возможности измерить, но подозреваю, что частично Hadoop и другие системы Big Data получили репутацию медленных из-за того, что их ЦП ограничен неоптимальным использованием сжатия.

По моему опыту, многие программисты не понимают компромиссов и нюансов сжатия и/или им не хватает знаний о существовании более современных и совершенных библиотек сжатия. Мнение общества таково: сжатие — это хорошо, используй сжатие [zlib]. Как и во многих аспектах программирования, в реальном мире есть множество тонкостей и нюансов. Динамика относительной мощности и затрат компонентов компьютера сдвинула маятник в сторону увеличения, а не экономии затрат при сжатии. Усугубляет ситуацию и то, что отрасль до сих пор активно использует тридцатилетний формат сжатия (DEFLATE/zlib), далёкий от идеала для современных компьютеров. Если вы займётесь измерениями, то найдёте множество ситуаций, в которых сжатие или не рекомендуется, или выиграет от использования более современной библиотеки сжатия (например, zstandard).

Двоичные файлы x86_64 в пакетах дистрибутивов Linux


В дистрибутивах Linux часто имеются заранее собранные двоичные файлы, устанавливаемые через программы для работы с пакетами (например, apt install или yum install).

Чтобы не усложнять и обеспечить максимальную совместимость, эти заранее собранные двоичные файлы собираются таким образом, чтобы запускаться на максимально возможном количестве компьютеров. В настоящее время многие дистрибутивы Linux (в том числе RHEL и Debian) имеют двоичную совместимость с первым процессором x86_64 — AMD K8, выпущенным в 2003 году. В этих процессорах появились современные наборы команд наподобие MMX, 3DNow!, SSE и SSE2.

Это означает, что по умолчанию двоичные файлы в составе многих дистрибутивов Linux не содержат команды из современных архитектур наборов команд (ISA). Никаких SSE4, AVX, AVX2 и подобного. (Строго говоря, двоичные файлы могут содержать более новые команды. Но с большой вероятностью они не будут находиться на путях выполнения кода по умолчанию и будет присутствовать код диспетчеризации во время выполнения для выбора их использования.)

Более того, компиляторы C/C++ (наподобие Clang и GCC) тоже по умолчанию в качестве целевой платформы используют уровень древней микроархитектуры x86_64 (отсюда и берутся параметры по умолчанию совместимости двоичных файлов дистрибутивов). Поэтому если вы компилируете собственный код и не указываете параметры типа -march или -mtune для изменения стандартных параметров целевых платформ, то скомпилированные двоичные файлы не будут использовать SSE4, AVX и т.п. Вы всё равно можете заставить своё приложение отправлять эти команды в динамических путях выполнения кода без переопределения -march/-mtune. Но для этого вам понадобится повышать сложность кода.

Из-за используемых по умолчанию параметров целевых микроархитектур в компиляторах и двоичных файлах дистрибутивов почти двадцать лет работы над ISA и повышением эффективности при помощи мощных ISA (например, сверлинейных векторизированных команд) остаются без дела. Меня раздражает, когда мои PR висят непросмотренными больше дня. А представьте, каково это — быть инженером AMD или Intel и знать, что для крупномасштабного внедрения твоей работы над ISA потребуются десятилетия!

Честно говоря, я не знаю точно, какую часть производительности мы теряем из-за этой обратной совместимости ISA. Это очень сильно будет зависеть от нагрузки. Но я не сомневаюсь, что есть очень крупные дата-центры, выполняющие требовательные к ЦП нагрузки, которые могли бы сильно повысить эффективность при помощи современных ISA. Если у вас работают тысячи серверов и нагрузка на ЦП обеспечивается не JIT-языком наподобие Java (JIT могут отправлять команды для машины, на которой запущены, потому что они компилируются точно в срок), то крайне рекомендую компилировать тяжёлые для ЦП пакеты (и их зависимости, конечно) из исходников, целевым для которых является уровень современной микроархитектуры, чтобы вы могли пользоваться преимуществами современных ISA. Но имейте в виду: использование современных ISA — это не «серебряная пуля»! Выполнение некоторых команд на самом деле может привести к снижению частоты ЦП, из-за чего использующий эти команды код может стать быстрым, а остальной код — медленным.

Поддержание совместимости двоичных файлов с исчезающе малым количеством древних ЦП ценой производительности на современных ЦП кажется… спорным решением. К счастью, дистрибутивы Linux и Clang/GCC обращают на это внимание.

GCC 11 и Clang 12 определяют уровни архитектуры x86_64-{v2, v3, v4}, целевыми платформами для которых являются Nehalem (выпущена в 2008 году), Haswell (выпущена в 2013 году) и AVX-512 (примерно 2015 год). То есть можно добавить -march=x86_64-v3, чтобы целевыми архитектурами стали процессоры с эпохи Haswell и далее, а компилятор создавал SSE4, AVX, AVX2 и другие современные команды.

RHEL9 повысит минимальные требования к архитектуре с x86_64 до x86_64-v2, то есть, по сути, будет требовать ЦП от 2008 года и более новые, а не от 2003 года.

Если вы хотите подробнее узнать об этой теме, начните с этой статьи Phoronix и переходите по ссылкам на другие статьи и обсуждения в списках рассылки.

Стоит заметить, что на момент написания статьи EC2-инстансы четвёртого поколения AWS (c4, m4, and r4) поддерживают AVX2 и, кажется, совместимы с целевой архитектурой x86_64-v3 GCC/Clang. А инстансы Intel пятого поколения имеют AVX-512, что теоретически делает их совместимыми с x86_64-v4. Поэтому даже если дистрибутив использует в качестве целевой платформы x86_64-v2, всё равно есть потенциал бесплатной производительности более новых ISA.

Если бы я управлял набором серверов, состоящим из тысяч машин, у меня было бы сильное искушение скомпилировать все пакеты из исходников, выбрав в качестве целевого уровень современной микроархитектуры. Это было бы затратно с точки зрения сложности, однако при некоторых нагрузках рост производительности стоил бы усилий. А такая стратегия консервативного выбора целевой платформы может оправдать запуск оптимизированных под современность дистрибутивов Linux или дистрибутивов Linux поставщиков облачных (например, Amazon Linux). Не уверен, выиграют ли от этого дистрибутивы наподобие Amazon Linux. Если нет, то разработчикам стоит изучить эту возможность!

В следующем разделе есть пример того, как неиспользование современных ISA оборачивается снижением производительности.

Многие реализации Myers Diff и других строчных алгоритмов Diff


Это достаточно специфическая тема, но я нахожу её наглядным примером, потому что здесь поведение достаточно контринтуитивно!

Разным видам ПО необходимо получать два текстовых документа и создавать текстовый diff их содержимого. Допустим, вспомните, что отображает git diff.

Для генерирования diff текста есть различные алгоритмы. Вероятно, самый знаменитый — это Myers Diff. Время выполнения алгоритмов пропорционально количеству строк. Скорее всего, O(nlog(n)) или O(n^2).

Эти текстовые алгоритмы поиска различий часто работают на уровне строк (а, допустим, не на уровне байтов или элементов кодового пространства), потому что это значительно ограничивает пространство поиска и минимизирует n, чтобы обеспечить хорошее время выполнения алгоритма.

За долгие годы многие люди осознали, что при поиске различий между двумя текстовыми документами большие части данных на входе почти одинаковы (кто будет искать различия между никак не связанным содержимым?). Поэтому в большинстве реализаций алгоритмов diff есть множество оптимизаций, ограничивающих количество сравниваемых строк. Две популярные оптимизации заключаются в определении и исключении одинакового префикса и суффикса данных на входе.

Если говорить очень просто, алгоритмы поиска различий в тексте часто работают так:

  1. Разделяем данные на строки.
  2. Хэшируем каждую строку для упрощения быстрого тестирования эквивалентности строк (сравнение контрольной суммы в u32 или u64 намного быстрее, чем memcmp() или strcmp()).
  3. Обнаруживаем и исключаем строки общего префикса и суффикса.
  4. Передаём оставшиеся строки алгоритму поиска различий.

Идея заключается в том, что шаги 1-3 (которые должны выполняться за O(n)) снижают объём работы алгоритма (шаг 4) со сложностью времени выполнения выше, чем O(n). Теоретически выглядит хорошо.

Но что происходит на самом деле?

Если профилировать несколько таких реализаций diff, то выяснится, что шаги 1-3 на самом деле занимают больше времени, чем предположительно медленный/затратный алгоритм! Как такое может быть?!

Одним из виновников является разбиение на строки. Даже если допустить, что мы можем использовать 0-copy/ссылки в памяти для хранения содержимого строк (а не выделять новую строку для хранения каждой распарсенной строки, что может быть намного менее эффективно), разделение текста на строки может быть ужасно неэффективным!

Причины могут быть разными. Возможно, вы декодируете текст в элементы кодового пространства, а не работаете с байтами (для поиска новых строк вы не должны обязательно декодировать все входящие данные). Возможно, вы проходите по файлу по одному символу/байту за раз в поисках LF.

Эффективным решением этой проблемы будет использование векторизированных команд ЦП (например, AVX/AVX2), которые могут сканировать несколько байтов за раз, ища контрольное значение или сопоставляя байтовую маску. Поэтому вместо 1 команды на байт входящих данных у вас будет 1/n. Вероятно, в вашей библиотеке времени выполнения C есть ассемблерные реализации memchr(), strchr() и похожих функций, и она автоматически выбирает самые новые/быстрые ассемблерные инструкции/команды, поддерживаемые ЦП времени выполнения (так делает glibc).

Теоретически, компиляторы распознают такие паттерны и автоматически создают современные векторизированные команды. Однако в реальности из-за того, что целевые ISA компиляторов по умолчанию достаточно древни по сравнению с тем, на что способен ваш ЦП (см. предыдущий раздел), вам остаются старые команды и линейное сканирование. Лучшим вариантом для вас будет использование функций во время выполнения C, которые предположительно поддерживаются ассемблерным кодом. (Однако аккуратнее с лишней тратой ресурсов на вызов функций.)

Ещё одним виновником неэффективности является хэширование каждой строки. Хэширование выполняется для сведения тестирования эквивалентности к сравнению u32/u64 вместо выполнения strcmp(). Похоже, многие реализации не уделяют внимания алгоритму хэширования и используют что-нибудь типа crc32 или djb2. Неэффективность здесь заключается в том, что многие старые алгоритмы хэширования работают на уровне байтов: им нужно передавать по одному байту за раз, обновлять состояние (часто используется XOR), а затем передавать следующий байт. Это неэффективно, поскольку не используется конвейерная обработка команд и суперскалярные свойства современных ЦП. Лучше использовать алгоритм хэширования, одновременно получающий по 4, 8 или более байт. Повторюсь, это снижает время выполнения с ~n тактов на байт до ~1/n.

Ещё один распространённый источник неэффективности — вычисление строк и хэшей содержимого в общем префиксе и суффиксе. Более эффективно использование memcmp() (или даже лучше: собственного ассемблерного кода, дающего смещение первого различия), потому что библиотека времени выполнения C, скорее всего, имеет реализации на ассемблере memcmp(), способные сравнивать входящие данные почти с нативной скоростью памяти.

Мне очень нравится этот пример, потому что он демонстрирует, как нечто, кажущееся O(n) медленнее, чем O(nlog(n))/O(n^2). Так получилось потому, что часто результат оптимизации снижает n затратного алгоритма до столь низкого значения, что его вычислительная сложность тривиальна. Компиляторы, выбирающие в качестве целевых древние микроархитектуры и не использующие векторизированные команды, обеспечивающие сверхлинейную производительность, ещё сильнее сдвигают время в сторону оптимизаций O(n).

Вывод


Компьютеры и ПО по неожиданным причинам могут быть неожиданно медленными. Хотя мой пост был длинным и в нём затронуто множество тем, он едва коснулся поверхности всех потенциальных вопросов. Я с лёгкостью могу найти ещё 10 тем, о которых стоит написать. Но это придётся оставить на другой пост.

Кроме того, компьютеры и ПО сложны. Когда дело касается производительности и оптимизаций, следует всегда измерять результаты. Описанные мной проблемы могут проявляться в вашем ПО и средах, однако попытки их решения могут и не оправдывать отдачи. Компьютеры и ПО, как и жизнь, полны компромиссов. Производительность — это только один из компромиссов. Не делайте из моих советов карго-культа и всегда пользуйтесь критическим мышлением.

Комментарии (87)


  1. dom1n1k
    17.08.2021 11:59

    Я назвал пост Surprisingly Slow

    Есть ещё https://accidentallyquadratic.tumblr.com/


  1. edo1h
    17.08.2021 12:03
    +1

    Задайтесь вопросом: что самое плохое может случиться, если отключится питание машины и потеряются данные в локальной файловой системе? В большинстве случаев ответом будет ничего. Вы спроектировали свою систему как stateless и устойчивую к сбоям. Вы управляете серверами как cattle. Вы работаете с локальными файловыми системами как с временными устройствами. Поэтому если машина сбойнёт, вы создадите новую ей на замену.

    так-то оно так, но оно работает только вы узнаете о порче данных (ну или вы перезаливаете данные после каждой нештатной перезагрузки).


    поясню: из-за переупорядочивания запросов может оказаться, что N последних запросов успешно записались на диск, а один запрос перед ними — нет. после перезагрузки кратко проверяется состояние файловой системы/БД/etc, последние изменения выглядят нормальными, полный тест никто не делает — слишком долго, да, зачастую, и механизмы не предусмотрены. в результате, то, что часть запросов на запись «потерялась», может остаться незамеченными.


    1. kolu4iy
      18.08.2021 11:49

      А в журнал они как записываются? Тоже будучи переупорядоченными?


      1. arheops
        18.08.2021 15:48

        Очень сильно зависит от настроек и типа БД.
        В mysql за последний год я два раза ловил неремонтируемую базу данных, благо делаю бекапы нормальные.


        1. kolu4iy
          18.08.2021 18:49

          Вообще мне интереснее был именно журнал ФС - с бд мне более-менее понятно как все работает, даже если переупорядочить. Точнее, как это не допустить: в продакшне мои сервера все стоят с noop io scheduler.


          1. arheops
            18.08.2021 19:39

            Scheduler тут вроде как вообще не особо важен(он влияет на block device). Все современные базы данных делают запись в несколько потоков и почти все — еще и в лог пишут в несколько потоков.
            Ибо SSD в вариантах «пишем в один поток» и «пишем в 10 потоков» дают совсем разные результаты.
            Журналы FS работают тоже неупорядочено кроме logfs и BTRFS(не со всеми настройками).
            Вообще базы данных должны лог писать в другую FS как минимум. В идеале — на отдельный и более быстрый накопитель(NVME?)


            1. kolu4iy
              19.08.2021 15:13

              М-м-м.... Я вот не очень уверен про несколько потоков в лог. Собственно, без обеспечения правильной последовательности данных в логе, никакого ACID не будет.

              Я не говорю, что никто не научился так делать, но можно пожалуйста примеры - почитать, что я пропустил?

              Ну и переупорядочивать запись, обеспечивая равномерный (или ещё какой, какой надо) отклик того самого block device (а ssd уже не block? а кто?) призван как раз io scheduler. А какой ещё уровень ОС переупорядочивает запись? Контроллер SSD может, но он не ОС...


              1. arheops
                19.08.2021 15:30

                Так происходит, к примеру, когда в mysql выставлено binlog_commit_wait_usec=50000
                и write_threads больше 1.
                порядок попадания в лог будет вполне случайный.

                База данных может(и будет) переупорядочивать запись.
                Сброс dirty buffer в файловой системе — тоже.
                Просто потому, что так проще реализовать. Сброс будет выполнен по порядку размещения в памяти.


      1. edo1h
        18.08.2021 19:05

        запись в журнал (файловых систем, баз данных) как раз обычно синхронная. а тут предлагают просто «оторвать» эту синхронность не глядя.
        нет, я не спорю, это может быть вполне разумным подходом, но нужно понимать возможные последствия.


  1. yurybx
    17.08.2021 13:22
    +4

    Интересно, почему медленный medoc?


    1. etozhesano
      17.08.2021 13:52
      +3

      Потому что криво написан. А ещё он оставляет за собой кэш хвосты в скрытой директории. И если не чистить, то ваш диск (системный) очень быстро заполнится.

      Криво написан - это мне говорил человек, который общался с разрабами медка. Говорят такое легаси и bad practice, что уже надо с 0 писать. Ну понятно что с 0 ничего не будет, пока окончательно не умрёт


  1. maledog
    17.08.2021 13:58
    +5

    Странно что большинство из описанных причин "неожиданные". Программист не знает про особенность создания процессов в windows, влияние антивируса или "индексатора" файлов на скорость закрытия, что вывод информации в стандартный вывод обычно блокирующий? Или что на многократный вызов скриптового интерпертатора уйдут ресурсы?


    1. F0iL
      17.08.2021 16:33
      +2

      Сила привычки. Простой пример: вы изначально разрабатывали свой софт под Linux и всё работало отлично, а потом решили его портировать под Win... и ой.


      1. maledog
        17.08.2021 17:34
        +1

        В linux тоже создание процессов и массовое открытие/закрытие файлов не бесплатные(более дешевые, но не бесплатные) потому всегда нужно думать о ресурсах которые израсходует приложение. Даже в linux рано или поздно упрешься в их нехватку.


        1. tbl
          17.08.2021 23:26
          +1

          ulimit есть: если файловых дескрипторов мало (во многих дистрибах по дефолту 4096 доступно, хотя сейчас уже и не 90-е годы), можно лимит увеличить, файловые дескрипторы в таблице ядра лежат и потребляют память (а не CPU), чего на современных компьютерах много.

          Не зря в wine esync сделан через файловые дескрипторы. Так проще всего оказалось управлять примитивами синхронизации в ntdll (через select/poll/epoll на группе fd), практически не нагружая wineserver, что дает приличный профит в многопоточных приложениях, включая реализации d3d9/10/11 через vulkan.


          1. maledog
            18.08.2021 10:36

            Я говорил про то, что если не думать о ресурсах при проектировании, то рано или поздно при масштабировании упретесь в их нехватку и невозможность дальнейшего масштабирования сколько бы мощными не были компьютеры и серверы эта мощность конечна.


    1. Fahrain
      17.08.2021 18:51
      +11

      Современные ОС и их api уже настолько большие, что никто чисто физически уже не может быть в курсе о всех потенциальных проблемах, чтобы корректно писать код. Поэтому общепринято полагаться на то, что в используемых библиотеках кто-то уже всё сделал правильно. К сожалению, в реальной жизни это оказывается не так в большом количестве случаев.


      А по работе с дисками и файловыми системами всё настолько сейчас плохо, что тут, наверное, уже можно многотомник написать про то, как делать не надо. При этом даже "правильные" решения всё равно работают просто адски медленно — и проблема не в дисках.


      Вот у меня есть винт с ~15 млн файлами. Размер MFT там — 14,57 Гб. Этот диск вообще невозможно дефрагментировать — ни один софт, который я бы ни пробовал просто не в состоянии это сделать за время, измеряемое не годами.


      И вот у меня есть самописная программа на .net, которая должна пробегать по этим файлам в поисках появившихся новых, о которых еще нет записей в базе. Ну, не суть важно зачем это нужно, дело не в этом. Дело в том, что только процесс сканирования папки с примерно 5-8 млн файлов (где и происходят основные добавления файлов) оптимизированным алгоритмом занимает от получаса до часа. И это мне всего лишь нужны имена файлов и их размеры! При этом если использовать стандартные api .net — процесс займет в 2 и более раз больше времени (они читаю больше данных, чем мне реально надо -> требуется больше времени). В чем заключается оптимизация? Да просто дергается напрямую функция FindNextFile из kernel32.dll. Причем, судя по всему, это — максимально доступная оптимизация в windows. Дальше только учиться вручную читать байтики из записей ntfs. Кстати, eplorer работает еще "быстрее" — ведь ему еще и на права доступа посмотреть надо.


      Замечу, что прочитать все 14,57 Гб MFT с диска можно за время, которое измеряется минутами, никак не теми часами, которые требуются для сканирования содержимого папки в объективной реальности.


      В итоге, пока у вас тысячи и десятки тысяч файлов — это всё еще более-менее быстро работает. А вот когда у вас миллионы — наступает ой. Но столкнутся с такой экзотической проблемой — единицы, а поймут, что именно можно оптимизировать — еще меньшее количество людей.


      1. Temtaime
        17.08.2021 22:40
        +1

        Всё начинает сильно тормозить, если у вас файлики лежат кучей в одной папке.
        Если вы не раскидываете файлики в подпапки — попробуйте это делать, чтобы в каждой папке было не особо много файлов(~50 тысяч).
        Например, можно создавать подпапки по первым двум-трём буквам имени файла. Если файлов всё равно много — создаёте в подпапке другие подпапки со следующими буквами.
        Так гит хранит блобы, например.


        1. Fahrain
          17.08.2021 23:12
          +3

          Там куча папок, внутри подпапки, ну — уровней десять точно есть. Файлы лежат только в конечных папках в цепочке, внутри 60-300 штук, не более.


          Вообще, проблема не в том, как файлы лежат. Проблема в том, что список файлов в 21ом году мы все еще получаем парой функций FindFirst/FindNext, идеология которых тянется из седой древности времен первых MSDOS, если не раньше. Оно в принципе не предназначено для какого-то оптимального по скорости чтения списка файлов. Поверх всего этого за 30 лет навернули десяток уровней кеширования, упреждающего чтения и слоев абстракций, но внутри — вот это вот древнее. Поэтому чтобы получить список содержимого папки надо делать кучу рандомных чтений по диску с десятком приседаний на каждый файл в угоду безопасности, вместо того, чтобы считать требуемые данные одним куском, еще и оптимизировав доступ с учетом особенностей движения головки диска...


          И, кстати, насколько я помню — в линухах используют примерно такую же пару функций, со всеми вытекающими последствиями.




          А так-то у меня достаточно хитрая задачка: мне надо искать папки, в которых есть дубли файлов. Т.е. есть папки с файлами на одном диске, надо проверить, что на втором диске (том самом, с кучей файлов) еще нет папки с такими же файлами. Ну там хеши считаются и т.п. Чтобы не считать их повторно — хеши от оригинальных файлов я в базу кладу, но так как эта папка пополняется не постоянно и наскоками — делать это в реальном времени через, например, драйвер-фильтр файловой системы смысла не имеет. Приходится сканировать список файлов и сверять с тем, что уже в базе есть. Чтобы повторно не пересчитывать — сначала смотрю на имя файла и размеры, если различаются или таких нет — пересчитываем хеши, обновляем в базе. Соответственно, входящие папки сверяем уже с базой, а не с живыми файлами с диска.


          Так вот, главная задержка не в том, чтобы хеши у новых файлов посчитать. Нет! Главная проблема — просто перебрать список файлов. Если там вообще не будет новых файлов, то процесс занимает 30 минут минимум — ну, диск на 5400 оборотов + еще и фрагментированный, что тоже добавляет задержек. Но основные проблемы тут именно в базовых функциях ОС, не в диске. Даже если диск сильно фрагментировать, мы все равно получим скорости чтения порядка 20-30 мб/сек. И если предположить, что из тех 14,57 гб MFT 2/3 — наша папка, то даже при таких скоростях требуемый объем прочитается за 8-10 минут. Но MFT обычно лежит крупными кусками, т.е. у нас будет чтение на всей скорости диска, 90-180 мб/сек (смотря какой диск и где файл), т.е. процесс потенциально должен занимать 1-2 минуты, а не 30.


          1. DCNick3
            17.08.2021 23:27

            Можно пойти чуть глубже и использовать нативные API windows: NtCreateFile и NtQueryDirectoryFile, подсмотрев их использование в ReactOS. Не знаю насколько это быстрее, но есть вариант прочитать все имена файлов сразу. Правда, не факт, что это будет быстрее, random read'ов всё равно много будет


            1. Fahrain
              17.08.2021 23:42
              +1

              Ну там, кажется, еще проблема в самой ntfs есть, т.к. часть данных о файлах лежат в виде отдельных кластеров, в которых хранятся запись о папке — если влезают. Т.е. вместо чтения общей таблицы файлов дополнительно приходится читать кластеры с данными размазанными по всему диску… Впрочем, не помню уже точно, могу и ошибаться.


              В любом случае, вы просто представьте, сколько в системе софта, которому надо получать списки файлов. И каждый делает это предельно неоптимальным способом, просто потому что он — стандартный! А задержки-то суммируются и накладываются друг на друга...


            1. DCNick3
              09.11.2021 13:37
              +2

              Вот что мне подсказали знающие люди:



              Мне известно несколько проектов, которые заменили FindFirstFileExW(, FindExInfoBasic,, FindExSearchNameMatch,, FIND_FIRST_EX_LARGE_FETCH) на вызов NtQueryDirectoryFile() напрямую, и получили прирост производительности:


              Но при таком низкоуровневом программировании (снятие слоёв абстракций) появляются дополнительные нюансы. Вот парочка из них:

              • Нужно помнить, что багнутый драйвер может переполнить переданный в NtQueryDirectoryFile() буфер — Crash on VirtualPC (Virtual Machine Folder Sharing Driver):
                • основные моменты первого сообщения:
                  So: we detected a heap corruption during freeing a block of heap memory — inside FindClose API call.

                  8-byte header + 0x1000 requested bytes + 16-byte footer

                  Library ntdll.dll checks footer bytes before freeing a heap block — values other than 0xAB mean that a buffer overflow has occurred

                  After few calls to NtQueryDirectoryFile I noticed that the footer of our 0x1000-byte block of memory has been overwritten with four zeroes
                • продолжение:
                  Now some details:
                  As I checked in the code of mrxvpc.sys, the bug is specific to ntdll.dll!NtQueryDirectoryFile function, no other functions are affected. NtQueryDirectoryFile calls a filesystem driver, in our case — for shared drive letters — the buggy mrxvpc.sys, which may overflow data buffer supplied by NtQueryDirectoryFile.

                Следовательно, необходимо аккуратно выбирать местоположение буфера в памяти:
                • хранение буфера в стеке нити — это очень плохая идея;
                • при использовании VirtualAlloc() (гранулярность 64 KiB) — позаботиться о корректной обработке переполнения буфера;
                • при использовании готовых (сторонних) аллокаторов памяти — включить проверку переполнения буфера.
              • Если посмотреть на официальное описание функции NtQueryDirectoryFile() (MSDN Microsoft Docs), и сравнить его с описанием и кодом этой же функции в Windows Research Kernel (WRK v1.2), то внимание привлечет различие в описании параметра RestartScan:
                • Microsoft Docs (GitHub):
                  RestartScan [in]
                  Set to TRUE if the scan is to start at the first entry in the directory. Set to FALSE if resuming the scan from a previous call.
                  When the NtQueryDirectoryFile routine is called for a particular handle, the RestartScan parameter is treated as if it were set to TRUE, regardless of its value. On subsequent NtQueryDirectoryFile calls, the value of the RestartScan parameter is honored.
                • WRK v1.2:
                  RestartScan — Supplies a BOOLEAN value that, if TRUE, indicates that the
                  scan should be restarted from the beginning. This parameter must be
                  set to TRUE by the caller the first time the service is invoked.
                  NtQueryDirectoryFile()BuildQueryDirectoryIrp():
                      if (RestartScan) {
                          irpSp->Flags = SL_RESTART_SCAN;
                      }
                  
                  т.е. флаг SL_RESTART_SCAN устанавливается только при явном задании.

                Возможно, где-то дальше FileHandle проверяется на «новизну» (первое использование в «service»), и устанавливается SL_RESTART_SCAN, т.е. в WRK v1.2 дано некорректное описание. Но возможно, и что в какой-то момент поведение функции NtQueryDirectoryFile() изменилось (в Microsoft Docs описано новое поведение). К сожалению, основываясь только на информации из документации, не получится определить версию системы, в которой изменилось поведение:
                • в Microsoft Docs написоно только:
                  Minimum supported client: Available starting with Windows XP.
                • WRK v1.2это NT 5.2:
                  The Windows Research Kernel v1.2 contains the sources for the core of
                  the Windows (NTOS) kernel and a build environment for a kernel that will run on
                  x86 (Windows Server 2003 Service Pack 1) and
                  amd64 (Windows XP x64 Professional)

                Можно посмотреть, как используют NtQueryDirectoryFile() в сторонних проектах:
                • ReactOS при первом вызове NtQueryDirectoryFile(…,TRUE)FindFirstFileExW()) следует описанию из WRK v1.2;
                • а упомянутые выше git-for-windows и vs-chromium — следуют описанию из Microsoft Docs, т.е. всегда вызывают NtQueryDirectoryFile(…,FALSE):

                  То есть, возможно, они не смогут корректно работать ни под ReactOS, ни под WRK v1.2, ни …

                  Возвращаясь к 18%, 30-40% приросту производительности — надеюсь, что при замерах производительности они проверили, что обработалось столько же файлов/директорий, сколько было при использовании FindFirstFileExW()



              …, но есть вариант прочитать все имена файлов сразу.
              FindNextFile() тоже читает (во внутренний буфер) сразу несколько «файловых записей» — столько, сколько поместится в буфер:






              И ещё один часто задаваемый вопрос (обычно его задают те, кто пишет драйверы, т.е. те, кто часто работают с Zw* и Nt* функциями):
              Почему FindFirstFileEx() запрашивает только одну «файловую запись» (NtQueryDirectoryFile() вызывается с (,,,,,,,,ReturnSingleEntry=TRUE,,))? То есть вместо того, чтобы прочитать всё содержимое небольшой директории в первом же вызове FindFirstFileEx() (за минимум 2 переключения контекста: user_space→kernel_space→user_space) приходится вызывать FindFirstFileEx();FindNextFile(); (за минимум 4 переключения контекста: user_space→kernel_space→user_space→kernel_space→user_space)⁉︎

              Оказывается, что в прикладном ПО FindFirstFileEx() часто используется, чтобы определить, пуста ли директория. Поэтому для уменьшения задержки (если в директории есть файлы, то из MFT считывается только одна запись (соответствующая первому файлу), вместо чтения множества записей, раскиданных по разным местам внутри MFT) добавили эту оптимизацию. Также бонусом это уменьшило количество используемой памяти при рекурсивном обходе директорий, в случае, если в директории находится только одна поддиректория, например: <несколько директорий>\<одна директория>\<несколько директорий>\<одна директория>\….

              А необходимость определить «пуста ли директория» чаще всего возникает при отображении TreeView элемента — нужно определить, показывать ли значок ⊞ рядом с папкой или нет.




              Начиная с Windows 8¹ для user-space в Win32 появилась универсальная функция GetFileInformationByHandleEx() (директория — это тоже «файл»; некоторые возвращаемые ею структуры аналогичны структурам NtQueryDirectoryFile()) — см. раздел «Remarks»: любой FileInformationClass (File*Info), который имеет пару в виде класса File*RestartInfo, перечисляет содержимое дирректории.
              Краткое описание всех FileInformationClass, включая поведение функции при их использовании, находится здесь.

              Кстати, в описании структуры FILE_FULL_DIR_INFO описано отличие класса FileFullDirectoryInfo от FileIdBothDirectoryInfo с точки зрения задержек доступа к данным:
              The FILE_FULL_DIR_INFO structure is a subset of the information in the FILE_ID_BOTH_DIR_INFO structure. If the additional information is not needed then the operation will be faster as it comes from the directory entry;

              FILE_ID_BOTH_DIR_INFO contains information from both the directory entry and the Master File Table (MFT).

              ¹ — сама функция доступна начиная с Windows Vista (…, Windows Server 2003 и Windows XP), но некоторые классы (в частности FileFullDirectory*) доступны только начиная с Windows 8:
              Windows Server 2008 R2, Windows 7, Windows Server 2008, Windows Vista, Windows Server 2003 and Windows XP: This value is not supported before Windows 8 and Windows Server 2012


          1. mvv-rus
            18.08.2021 00:57
            +1

            Но MFT обычно лежит крупными кусками

            Не поменялось ли что в последних версиях ядра — не смотрел, но раньше для MFT была специальная резервная область, куда она росла, а потому доступ к ней был линейным. Но все это было хорошо, пока размер MFT не становился больше резервной области — после чего MFT росла так же, как и любой другой файл: по мере необходимости, в свободном на данный момент месте. Другой способ получить тот же эффект состоял в том, чтобы забить диск почти до упора: в некоторый момент файловая система жертвовала этой резервной областью, после чего MFT опять-таки росла, как получится, и могла сильно фрагментироваться.
            Про фрагментацию MFT обычно можно было узнать из выдачи сторонней программы дефрагментации (вроде бы, в какой-то версии ее и встроенная показывала, но в Win7, например, я такого не увидел).
            Даже если диск сильно фрагментировать, мы все равно получим скорости чтения порядка 20-30 мб/сек

            Это — большой оптимизм, реально, по моим наблюдениям — чаще на порядок ниже.
            Если файл сильно фрагментирован, то скорость его чтения упирается в IOPS (I/O operations per second) диска (т.к. требуется поиск), а с IOPS у пятитысячника довольно кисло, ориентировочно — где-то в районе 50. И если MFT росла по кластеру за раз, то даже при самом большом кластере 64К и прочих идеальных условиях скорость чтения одной только ее не может быть больше 3,2 МБ/сек. А содержимое папки — оно не в MFT хранится, а в индексе (и читается, небось, произвольным доступом), что скорости не прибавляет.
            А если ещё и памяти под кэш маловато, чтобы MFT целиком оказалась в нем и никуда бы оттуда не уехала — все будет ещё медленнее и печальнее.
            PS Я подобную проблему видел у себя в давние времена, лет 15 назад, только там масштабы бедствия были поменьше — не миллионы файлов а немногие сотни тысяч в одной папке, а потому и времена были сильно меньше часа. Но вот если попытаться открыть такую папку в файловом менеджере (не только в Explorer, но и, к примеру, в Far) — там все было плохо.


            1. Fahrain
              18.08.2021 01:21

              Ну вот в целом — всё круто же, прям наглядно видно торжество алгоритмов)


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


              немногие сотни тысяч в одной папке, а потому и времена были сильно меньше часа. Но вот если попытаться открыть такую папку в файловом менеджере (не только в Explorer, но и, к примеру, в Far) — там все было плохо.

              Не, тут тоже весело, но причина попроще. Чтобы узнать кол-во файлов — надо просканить папку, чтобы ее просканить — надо получить все записи о всех файлах. Насколько я помню, другого способа узнать кол-во записей — нет. Это приводит к тому, что файловый менеджер должен выделять память в процессе сканирования папки — последовательно, относительно мелкими кусками (привет, фрагментация памяти!). Плюс само сканирование запрашивает много избыточной информации — даты создания/изменения, атрибуты, права и т.п. Всё это хранится в разных местах, все это надо прочитать, обработать, положить в память. А если сюда еще и вытеснение в своп добавить — ну, на старых системах и памяти было мало...


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


            1. Am0ralist
              18.08.2021 16:29

              PS Я подобную проблему видел у себя в давние времена, лет 15 назад, только там масштабы бедствия были поменьше — не миллионы файлов а немногие сотни тысяч в одной папке, а потому и времена были сильно меньше часа. Но вот если попытаться открыть такую папку в файловом менеджере (не только в Explorer, но и, к примеру, в Far) — там все было плохо.
              Но очистить папки темп при этом было проще ручками в тотальнике, чем открытием в эксплорере или через «очистку диска» винды)


          1. Teplo_Kota
            18.08.2021 13:44

            В приложении Everything, как я понял, в итоге плюнули на это всё. Они получают низкоуровневый доступ к диску и сами парсят NTFS.


            1. YuryB
              24.09.2021 17:34

              да, вот бы все так что-ли делали, работает феноменально быстро, она использует индекс, но строится он моментально. и ведь этой программе точно больше 10-и лет


      1. Imp5
        18.08.2021 01:18

        А что если скопировать прямо в проводнике на другой диск? Что-то вроде дефрагментации для бедных. Если будет копироваться по 200 файлов в секунду, то уйдут одни сутки.


        1. Fahrain
          18.08.2021 01:29

          Ну только это и остается, если задача именно дефрагментировать. Но в условиях наличия 15 млн файлов и постоянного постепенного пополнения их количества — смысла в дефрагментации, имхо, уже как-то не просматривается.


          Кстати, бонус: берем чистый свежеотформатированный раздел. Берем системную функцию копирования — ну или проводник. Копируем на этот винт, ну, например, видео на 1,5-5 гб размером. На выходе получаем файл, побитый на несколько фрагментов и раскиданный по всей поверхности диска. Казалось бы, что мешает его положить в начало/конец диска одним куском? Ан нет...


          Еще очень весело, если взять большой файл с крупными кусками данных, которые хорошо жмутся (недокачанный торрент?) и включить на нем галочку "сжимать содержимое". Файл будет порезан на кучу кусков, куски раскиданы по всему диску рандомно, дефрагментаторы как правило не в состоянии его собрать в какую-то более-менее компактную кучу — привет нелинейный рост фрагментации и далее, когда файл докачают/удалят.


          1. Imp5
            18.08.2021 01:31

            А что за задача такая? Почему именно файловая система, а не база данных?


            1. Fahrain
              18.08.2021 01:42

              Ну хранить бинарные данные (картинки) в базе данных — как-то не очень хорошо. Кроме того мы автоматом лишаемся кучи всякого полезного софта, который теперь придется писать самому, с учетом, что данные — в БД.


              Бонусом мы получаем проблемы с фрагментацией файлов уже самой базы данных (они решаются попроще, но всё равно ж есть) и потенциально кучу проблем с восстановление данных, если что-то сбойнет.


              Одно время я думал использовать альтернативы в виде архивов или монтируемого vhd-образа. Но всё упирается в вопрос, а как это восстанавливать, если что-то сбойнет или на диске появится бэдблок от времени?


              В итоге — так всё и осталось именно в виде кучи файлов. Пользоваться — удобнее, шансы вытащить данные при сбое — выше.


              1. 1dNDN
                18.08.2021 20:50

                WinRAR, например, умеет добавлять данные для восстановления в архив


                1. Fahrain
                  18.08.2021 20:54

                  Ну да, только они тоже могут быть повреждены. Пока у вас архив относительно маленький — шансы потерять данные тоже пропорционально маленькие. А если мы начинаем говорить о файле размером в 2-3 тб, то малейшее его повреждение в середине приводит к тому, что еще не поврежденные данные из него вытащить — очень сложно. Причем это вообще глобальная проблема для кучи форматов и в большинстве случаев даже соотв. софта для этого в природе не существует.
                  Вот в итоге вам что страшнее? Потерять 2-3 тб разом или десяток файлов на 100-200 кб из этого объема?


                  1. Am0ralist
                    19.08.2021 10:14

                    Вот да, я уже в другой теме обсуждал косяк с архивами в 2-3 местах, что в случае, если они повредятся все со временем, то нет даже ПО, чтоб попытаться собрать из этих трёх разных повреждённых архива один и вытащить по максимум всю инфу.
                    Хотя вот если задать большую область для восстановления, то шанс повредить и не вытащить вроде не настолько велик.


          1. netch80
            24.08.2021 11:35

            > Копируем на этот винт, ну, например, видео на 1,5-5 гб размером. На выходе получаем файл, побитый на несколько фрагментов и раскиданный по всей поверхности диска. Казалось бы, что мешает его положить в начало/конец диска одним куском? Ан нет…

            В книге «Design and implementation of 4.4BSD» в разделе про UFS писали, что специально переходят на следующую полосу (cylinder group) если файл заполняет более 1/2 полосы, и что это эмпирически дало заметное улучшение ситуации с фрагментацией.
            Возможно, тут похожие мотивы.


      1. maledog
        18.08.2021 10:56

        Может имеет смысл не пробегать по всем файлам а следить за их созданием и изменением при помощи ReadDirectoryChangesW?


        1. mayorovp
          18.08.2021 11:46

          Это требует чтобы программа была запущена всё время, а при запуске всё равно понадобится часовое сканирование.


          Лучше уж вот с этим разобраться: https://docs.microsoft.com/en-us/windows/win32/fileio/change-journals


          1. maledog
            18.08.2021 12:18

            Вариант - отдельная служба, которая ведет журнал изменений/Консольное приложение запускаемое через nssm как служба.


          1. mvv-rus
            18.08.2021 13:52

            Лучше уж вот с этим разобраться: docs.microsoft.com/en-us/windows/win32/fileio/change-journals
            Тут есть нюанс: этот журнал по размеру ограничен (причем, по умолчнию — ограничен сильно). И, к примеру, Служба репликации файлов, которая была на него завязана (она использовлась в AD для репликации шаблонов групповых политик), стоило ей полежать по недосмотру пару дней, часто нарывалась на переполнение этого журнала даже на контроллерах домена, где активность на системном диске обычно невелика (печально известная ошибка JRNL_WRAP_ERROR).
            Так что программу отслеживания лучше держать запущенной постоянно.
            Ну и при высокой интенсивности дисковых операций та же служба репликации файлов, но используемая для репликации DFS (т.е., фактически — содержимого общих папок на сервере) тоже могла не успеть обработать поток изменений и вываливалась с аналогияной ошибкой.
            Ну, и при некорректном завершении работы этот журнал не так уж редко портился.


    1. mayorovp
      17.08.2021 19:18

      влияние антивируса или "индексатора" файлов на скорость закрытия

      А где этому учат? И где во всех насквозь асинхронных фреймворках API для асинхронного закрытия файлов?


      1. mvv-rus
        18.08.2021 01:04
        +1

        А как их закрыть асинхронно? Системный вызов CloseHandle — он существенно синхронный, асинхронность к нему не приделаешь: она в системе на этот самый Handle завязана.
        Разве что как в статье — раскидать по пулу потоков.
        PS Интересно, если папку исключить из real-time сканирования — будет ли драйвер фильтра антивируса обрабывать закрытие файла существенно быстрее?


        1. Roman_Cherkasov
          18.08.2021 12:38

          Если я правильно понимаю, то антивирус влияет не только на скорость закрытия файлов. VSCode и IDE от JetBrains при первом запуске на винде спрашивают о том, стоит ли добавлять папку с проектами в исключения Windows Defender. Методом научного тыка выяснено, что если исключение не создавать - скорость работы падает. При чем вся, начиная от банального pip install, заканчивая pytest


      1. maledog
        18.08.2021 10:29

        С влиянием антивируса сталкивается каждый второй. Когда я работал админом, каждый второй программист приходил и требовал добавить его каталог в исключения антивируса "а то тормозит". Да даже на примере элементарного копирования каталога с большим количеством файлов видно, если работаешь с разными OS. А может не стоит хранить столько файлов в NTFS? VFS, как microsoft поступила с gitfs? Или упаковать их в базу данных, если это документы? Или выбрать FS которая более приспособлена для хранения? их десятки готовых.


        1. Gorthauer87
          18.08.2021 11:49
          +1

          Вот подобные тормоза и тугой терминал, причём pwsh вообще прямо неприлично долго стартует, и мешают мне с комфортом разрабатывать на Windows машине. Причём ситуация вообще не меняется, даже Windows Terminal её не спас, хотя жизнь и стала лучше.

          Думаю это немаловажный фактор, что в среде разработчиков так популярны Linux и Mac OS. И никакой WSL этого не исправит ведь.


          1. maledog
            18.08.2021 12:13

            WSL нет. Но WSL2 виртуальная машина и антивирус в нее лезть не должен. Хотя там тоже будет просадка из-за слоя виртуализации.


      1. maledog
        18.08.2021 11:13
        -2

        С точки зрения ОС и оборудования все ваши действия не асинхронны. Это нужно понимать.


        1. mvv-rus
          18.08.2021 14:02
          +2

          Неверно. С точки зрения оборудования HDD (и его совремнные эквиваленты), да и некоторые другие устройства — тоже, находят и передают в память запрошенные данные асинхронно, пользуясь прямым доступом к памяти, а процессор в это время выполняет что-то другое.
          С точки зрения ОС действия тоже могут быть асинхронными: в Windows асинхронные (без блокировки потока выполнения) операции ввода/вывода был изначально, начиная с самой первой версии NT, и в Linux они AFAIK тоже теперь реализованы.


        1. netch80
          24.08.2021 11:27
          +1

          > С точки зрения ОС и оборудования все ваши действия не асинхронны.

          Вот я, например, выдал aio_read() на блок данных и попросил нотификацию через kqueue. Запрос встал где-то в очередь.
          Запрос преобразовался в запрос к диску. У диска поддерживаются тегированные команды. Ушла команда «тег=4 прочитать блоки 35410800-35410815» по интерфейсу к диску. У диска в это время ещё ждут команды с тегами 0,1,6,7,19.
          Пусть диск — магнитный (HDD). Он заглянул в таблицу ремапа, проверил все полученные физические LBA и нашёл, что сейчас он двигается по возрастанию блоков, и команды упорядочённые в порядке возрастания физических LBA — 1,19,7,4,6,0.
          Ближайшим он отдаст содержимое блока 17254987 для команды с тегом 1. Потом блока 19443009 для команды с тегом 19. И так далее.
          На каждый прочитанный блок ОС будет размораживать какое-то ожидание и отдавать данные заказчику (юзерленд или ядерный драйвер, если это, например, структуры FS).
          Когда-нибудь дойдёт запрос и до нашей aio_read(). А тем временем процесс успел поставить в очередь ещё десяток aio_write(), которые дойдут до того же диска и будут отработаны в свою очередь.

          > Это нужно понимать.

          Что мы должны «понимать» в описанном и почему я не должен считать все эти операции асинхронными?


          1. maledog
            25.08.2021 10:38
            -1

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


            1. edo1h
              25.08.2021 11:36
              +2

              Заполнив очередь запросами вы получите торможение

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


            1. netch80
              25.08.2021 11:52
              +1

              > Заполнив очередь запросами вы получите торможение.

              Вы имеете в виду, что моя операция, которая поставлена позже, но для диска будет выглядеть как более ранняя к исполнению, отработается раньше, чем та, что я сам поставил раньше?
              Ну да, есть большая вероятность, что так произойдёт. И чем это плохо в общем случае?

              Разумеется, можно придумать любую самую кривую стратегию шедулинга запросов на любом уровне (очередь в приложении, в ОС, в контроллере диска, в самом диске и т.п.), что какие-то операции вообще остановятся. Есть типовые ошибки подобного рода. Но промышленно ценные реализации такого не допускают — они дают гарантии с чётко определёнными пределами времён отработки каждого запроса.

              В том же примере с HDD, если вы берёте следующий блок на выполнение как ближайший к текущему, это может заклинить его в узком участке, а если вы реализуете «лифтовый» оптимизатор, который сначала отрабатывает все поступившие запросы при движении вперёд, пока есть из-за чего двигаться, а потом — при движении назад, и так по кругу — будет и достаточно быстрое, и эффективное выполнение.

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

              Вы, наверно, имеете в виду вариант, когда из-за отсутствия приоритизации на нижнем уровне приходится реализовывать её на более высоком. Да, есть и такие варианты. Например, может быть полиси типа «на каждый запрос приоритета 2 отдавать не более 10 запросов приоритета 1 (ниже) в ОС; следующие тормозить у себя до получения ответа». (Я бы применил что-то в духе hierarchical token bucket с лимитированием неотвеченной полосы… но это всё тестить в реале надо.) Но доля случаев необходимости такой приоритизации, насколько я вижу, мала, и с ходу даже не вспомню приложение, которое бы делало что-то подобное. У Oracle DB такого нет?

              А если у вас есть логическая зависимость между запросами (как у СУБД, сначала пишется запись об изменении в журнал, потом в основное хранилище базы, и потом запись о завершении транзакции в журнал), то её и надо реализовывать напрямую через постановку следующей операции в коллбэке от предыдущей, а не просто через последовательность в очереди — которая в таких условиях и не должна работать.


              1. maledog
                25.08.2021 16:37
                -1

                Вот вы сейчас размышляете как оптимизировать работу приложения на разных уровнях. Мой изначальный посыл был о том, что об этом стоит задумываться. Даже если у вас весь из себя асинхронный фрэймворк выяснится что где-то на нижнем уровне какая-то его часть упирается во вполне синхронный процесс.


                1. netch80
                  25.08.2021 19:10
                  +1

                  > Мой изначальный посыл был о том, что об этом стоит задумываться.

                  Да. Но — в определённых типах задач — где подобные вопросы могут встать изначально.

                  > выяснится что где-то на нижнем уровне какая-то его часть упирается во вполне синхронный процесс.

                  Верно. Процессор выполняет команды вполне синхронно (не учитываем OoO, у него протяжённость невелика). Любой асинхронный процесс состоит из множества мелких синхронных кусков. Важно не тормозиться на них там, где этого не ожидаешь.


          1. edo1h
            26.08.2021 15:44

            Пусть диск — магнитный (HDD). Он заглянул в таблицу ремапа, проверил все полученные физические LBA и нашёл, что сейчас он двигается по возрастанию блоков, и команды упорядочённые в порядке возрастания физических LBA — 1,19,7,4,6,0.
            Ближайшим он отдаст содержимое блока 17254987 для команды с тегом 1. Потом блока 19443009 для команды с тегом 19. И так далее.

            (реплика, не имеющая прямого отношения к беседе)
            у меня есть впечатление, что современные hdd устроены хитрее, они учитывают не только и не столько близость дорожек, сколько то, над каким сектором окажется головка после перемещения.
            именно это отправило в отставку старое эмпирическое правило iops≈rpm/60, сейчас диски при достаточной глубине очереди выдают в разы больше.


            1. netch80
              22.09.2021 12:59

              > у меня есть впечатление, что современные hdd устроены хитрее, они учитывают не только и не столько близость дорожек, сколько то, над каким сектором окажется головка после перемещения.

              Это тоже достаточно старо — драйвер UFS на технике уровня PDP-11, VAX учитывал такое. В заголовке суперблока UFS поэтому были всякие rotational delay конкретной техники.
              Но на HDD с начала 90-х, с тем, как в диске разные области стали иметь разные временны́е плотности данных для уравнивания пространственных плотностей — эти данные драйверу уровня ОС уже недоступны. Уровень ремапа номера сектора не только производит подстановки в таблице замен, но и преобразует linear block address в комбинацию зона — цилиндр — головка — сектор, с тем, что разные зоны имеют разное количество секторов на дорожку. (Тогда же геометрическая адресация, видимая на шине, перестала иметь реальный смысл и осталась только как легаси.)

              > именно это отправило в отставку старое эмпирическое правило iops≈rpm/60, сейчас диски при достаточной глубине очереди выдают в разы больше.

              Да.


  1. tbl
    17.08.2021 14:17
    +2

    По поводу оптимизации fsync для stateless-серверов. Разговаривал как-то с программистом, который допиливал файловую систему как раз для таких виртуализованных систем облачного провайдера. Говорит, пришел в проект, а там все настолько было выкинуто из ядра, что даже порушили консистентность журналирования по метаданным: сервер уходит в перезагрузку, потом в него, если надо, заливаются новые версии снапшотов виртуалок, и сервер снова в строю, но базовая fs уже находится в неконсистентном состоянии. Сыпались сервисы из-за потери данных, со временем корраптилась файловая система, и даже понять не могли, почему. В итоге часть оптимизаций пришлось откатывать назад, потому что альтернативный workaround выходил слишком дорогим по времени - после любой случайной перезагрузки полная переналивка диска с нуля.


  1. qark
    17.08.2021 14:21
    +6

    В Ubuntu 21.10 пакеты будут сжаты zstd: https://balintreczey.hu/blog/hello-zstd-compressed-debs-in-ubuntu/


    1. tbl
      17.08.2021 14:26
      +1

      Да ща все дистрибы на него потихоньку съедут, zstd в виде консольной команды уже везде доступен, и tar уже ключ --ztsd более 3 лет понимает.


      1. chupasaurus
        17.08.2021 16:24

        Собственно багу в Debian от Canonical по поводу поддержки 3 года (и мимо 11 релиза).


  1. slonopotamus
    17.08.2021 14:39

    Возможно, в том числе из-за этого кажется, что Windows медленнее Linux.

    Слово "кажется" здесь лишнее, выше по тексту же чётко сказано что на Windows процессы создаются медленнее.

    в последнее десятилетие производительность одного ядра ЦП примерно находилась на плато.

    Ну да, конечно. Сравните показатели десятилетних Intel 2xxx с современными Intel 11xxx.


    1. Bonart
      17.08.2021 17:35
      +1

      Процессы в Linux создаются быстрее, чем в Windows, только если это несколько процессов с одним бинарником. Зато Windows потоки намного дешевле линуксовых. Отсюда следует, что замедление в Windows будет получено только для софта, плодящего процессы на каждый чих, вроде postgres


      1. slonopotamus
        17.08.2021 19:27
        +1

        Зато Windows потоки намного дешевле линуксовых

        Дадите какой-нибудь пруфлинк? Мне вот говорят что потоки под виндой тоже медленнее создаются.


        1. Bonart
          21.08.2021 15:15

          Скорость создания потоков, которую измерили по вашей ссылке, не имеет значения: потоки в подавляющем большинстве приложений берутся из пула.

          Значение имеет скорость переключения между потоками и поддержка множества потоков операционными системами.


          1. slonopotamus
            21.08.2021 15:27

            Ну вы ссылку на бенчмарки-то давать будете?


  1. HiMem-74
    17.08.2021 14:45

    Однажды столкнулся с дикими тормозами на ноуте (кажется это был Делл) с i5 на свежей Win10. В какой-то момент что-то пошло не так и ядра не поднимали частоту с 500 МГц в простое до положенных 2,2 ГГц в рабочем режиме. В какой момент это произошло - непонятно, пользователь грамотный, дичь не творил.
    Был перечитан весь интернет, испробованы все способы, ничего не помогло.
    Переустановка Win с нуля решила вопрос. Такая история.


    1. F0iL
      17.08.2021 16:36
      +1

      Однажды столкнулся с дикими тормозами на ноуте (кажется это был Делл) с i5 на свежей Win10. В какой-то момент что-то пошло не так и ядра не поднимали частоту с 500 МГц в простое до положенных 2,2 ГГц в рабочем режиме.

      На Dell'ах такое обычно происходит, когда ноутбук не может опознать блок питания как свой родной и тормозит процессор якобы из лучших побуждений. Для определения "своего" блока питания там используется дополнительный контакт в разъеме, и когда разъем либо штекер БП расшатывается/загрязняется, то начинается подобная радость.
      С переустановкой Windows тут скорее всего просто совпало.


      1. HiMem-74
        19.08.2021 08:03

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


  1. arheops
    17.08.2021 15:12
    +2

    Насчет «перекомпиляции» на amd_x64-v4 — это, конечно, в теории классно. Но на практике учитывая количество пакетов — практически всегда требует менять что-то в пакетах для совместимости, и не всегда это видно при компиляции. Разработчиков много, причем подавляющее большинство, включая пишущих ассемблерные вставки, не знает даже о самом факте возможности таких проблем.


    1. tbl
      17.08.2021 15:34

      судя по количеству пакетов в генту, в ебилдах которых есть фильтрация по cpu-флагам в компиляции - таких с гулькин нос, и все они давно известны.


      1. arheops
        17.08.2021 15:44
        +2

        Добавьте также пакеты в генту, в которых есть патчи генту.
        Я лично делал такое только с rhel пакетами, и там постоянно какие-то бяки вылазят.
        В общем такая операция требует до недели работы вполне себе дорогого специалиста с специфичными скилами.
        А разница в производительности — ну процентов 5-10 обычно.
        Плюс учтите, что с появлением апдейтов надо повторять.
        А если не повторять, то, к примеру, для mysql от апдейта может быть ускорение больше, чем от перекомпиляции с расширенными инструкциями.


  1. NumLock
    17.08.2021 16:06
    +3

    Последние оськи Windows живут своей сказочной жизнью отдельно от установленного ПО. Microsoft программеры наплодили столько сервисов, что процессор просто захлёбывается на стартапе. Использование affinity для svchost, выделяя им для работы пару процессорных ядер, иногда помогает. Компьютер оживает. GUI программ начинают работать с меньшими лагами. Однако, проги опирающиеся на backend начинают тормозить. Поэтому приходится периодически профилировать машину под определённые задачи.


    1. fmj
      18.08.2021 13:41
      +1

      не понятно, почему наплодив столько сервисов, они забили сделать хотя бы сниженный приоритет для них. Из всех системных, что видел, только cервис оптимизации томов работает со сниженным приоритетом, остальное - везде обычный.

      cами сделали уровни приоритета для цп, io и памяти - но ими не пользуются.


    1. rPman
      18.08.2021 18:28

      Это какие приложения backend из-за svchost начинают тормозить?
      или вы про торможение всех экщемпляров процесса а не только те что грузят машину обновлениями/телеметрией и службами, которые нужны только майкрософту.


  1. emerald_isle
    17.08.2021 17:18

    Если бы я управлял набором серверов, состоящим из тысяч машин, у меня было бы сильное искушение скомпилировать все пакеты из исходников, выбрав в качестве целевого уровень современной микроархитектуры.

    Интересно, по какой причине облачные провайдеры используют что-то подобное RedHat/Debian, но почти никогда Gentoo или Calculate? Понятно, что все эти флаги - штука специфичная (и не серебряная пуля, как написано выше), но там много и других преимуществ...


    1. RarogCmex
      17.08.2021 23:32
      -4

      Корпоративные макаки не осиливают USE флаги?


    1. arheops
      17.08.2021 23:33
      +1

      по очень простой причине.
      вот у меня сейчас есть система, в которой мне нужно поддерживать up-to-date состояние всех пакетов.
      в среднем меняется три пакета каждую неделю.
      а в случае генту это перекомпиляция трех пакетов каждую неделю. на каждой из десятка-сотни машин(ну или перенос организовывать).
      этим заниматься, конечно, можно. Но по факту это +один выделенный на задачу человек.


      1. ReDev1L
        17.08.2021 23:54
        -2

        Время своё не жалко? Гуглите ansible, пора уже выходить из пещеры.


        1. arheops
          18.08.2021 00:09

          Время не жалко, эта операция в контракте прописана как ручная и оплачивается по рейту 120 в час. К тому же за ней стоит ручная проверка состояния(автоматическая выполняется независимо, естественно, не руками), в которую входит sql profiling(вроде как не автоматизируется никак, не знаю как можно обьяснить системе какией из sql критично замедлилися, какие нет. Типа замедлилися, но система переживет).
          А вот времени делать компиляцию жалко, да.


      1. khajiit
        18.08.2021 13:55
        +1

        на каждой из десятка-сотни машин

        Свое зеркало с бинарями на выделенном билд-сервере не поможет?
        Такие и для дебианов делают, потому что сотни серверов, обновляющих один и тот же пакет — это чересчур. Для генту это делали еще лет 15 назад, когда этот компы крутил на полставки.


        1. arheops
          18.08.2021 14:11

          Поможет, если одинаковые окружения везде.
          В любом случае это не то, что делается пару часов в месяц.
          Не всегда стоят эти проценты мороки.


  1. diversenok
    18.08.2021 01:29
    +12

    Решил более детально замерить различные аспекты производительности создания процессов в Windows, вот мои результаты для Windows 10 20H2.

    Обычный запуск, система без антивируса:

    • ~7 мс на ShellExecuteEx - высокоуровневый API оболочки предназначенный скорее для открытия файлов в приложении по умолчанию, чем создания процессов.

    • ~1.1 мс на CreateProcess - основной документированный метод.

    • ~0.8 мс на NtCreateUserProcess - нижележащий системный вызов.

    • ~0.9 мс на NtCreateProcessEx + NtCreateThreadEx - альтернативный (более старый) способ, требующий больше действию вручную.

    Создание клона процесса (местный аналог fork), система без антивируса:

    • ~1.5 мс используя NtCreateUserProcess.

    • ~0.9 мс используя NtCreateProcessEx + NtCreateThreadEx.

    В то же время, создание нового потока занимает всего ~0.03 мс, что намного быстрее.

    Упомянутый выше NtCreateProcessEx имеет интересную особенность - он создаёт только объект процесса, без изначального потока. Поскольку драйвера, которые подписываются на синхронные уведомления о создании процессов (посредством PsSetCreateProcessNotifyRoutine) получают их только при создании первичного потока в целевом процессе, мы получаем возможность измерить "чистое" время создания процессов, без вклада от сторонних драйверов. В этом случае NtCreateProcessEx тратит ~0.6 мс.

    Наличие в системе дополнительных драйверов (вроде антивируса или EDR), может существенно повлиять на результаты. Так, включённый Windows Defender даёт примерно +90% замедления, заставляя CreateProcess тратить по ~2.1 мс. Но это ещё ничего, если взять какой-нибудь откровенно плохой антивирус, например, китайский 360 Security, то можно замедлить CreateProcess в 200 (!) раз, до ~240 мс.


  1. alliumnsk
    18.08.2021 12:16
    +2

    Я еще в 2005 году читал, что программы с нативным AOT-кодом скоро отомрут, из-за того что JIT может компилировать под текущий процессор, и будет работать быстрее, а все будут писать на Nemerle... Эх, ну и где оно? А MS добавила AOT-компиляцию в сишарп.


    1. Gorthauer87
      18.08.2021 12:37
      +2

      Да потому что влияние всех этих модных simd инструкций на скорость кода в целом небольшое, а там где они реально нужны, их обычно пишут руками.

      Помню юзал Gentoo и все собирал с march=native. Не особо это и помогало


  1. KES777
    18.08.2021 20:35
    +2

    Вы пишите, что вы мейнтейнер Mozilla или как-то связаны с ними.


    Может тогда пните их под зад, чтобы они начали поддерживать zstandart. A то как другое ПО будет использовать новомодные библиотеки, если Accept-Encoding: gzip, deflate?


    Спасибо ;-)


    1. ITLav
      20.08.2021 16:36

      И в firefox и в chromium вижу одинаковое
      Accept-Encoding: gzip, deflate, br

      Вроде как brotli (которое тут под псевдонимом br) достаточно прогрессивное сжатие.


      1. YuryB
        23.09.2021 19:16

        да, brotli жмёт на 20-30% лучше, но на максимальном сжатии httpd буквально вис и сайт раз в 5 медленнее открывался, как раз то о чём писал автор. по-моему проблемы были с большими js файлами. по факту нужно проводить очень скрупулёзное тестирование на предмет реальной выгоды и параметров. и интересно как модуль апача скомпилирован, с каким набором инструкций :)

        ну а сам brotli кстати все современные браузеры поддерживают, но им никто не пользуется (я вот о нём случайно узнал), собственно и об этом автор писал :)


    1. dbalabanov
      22.08.2021 12:25

      это же перевод