пятница, 28 апреля 2017 г.

Delphi, скорость и потоки. Одно вычеркните.

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

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

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

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

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

Скажем, в С++ можно завести массу небольших объектов на стеке. В каждом из них внутренние члены также объекты по значению, а не по указателю. В Delphi же это ВСЕГДА в динамической памяти. Чтобы не использовать динамическую память, нужно отказаться от использования классов и использовать только структуры. И не использовать дельфийские строки с автоматическим управлением длиной, а перейти на строки фиксированной длины. И автоматически отказаться от дельфийских библиотек, от базовых функций VCL, поскольку они езде используют и классы и строки переменной длины. То есть перейти на довольно жесткую версию оригинального паскаля и переписать (во многом) функции нижнего уровня, использованные приложением.

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

Впрочем, мне как сишнику претит уже само по себе необходимое требование заводить даже небольшие объекты не на стеке, а в динамической памяти и на ровном месте ни за что терять время на Create-Free.

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

Комментариев нет:

Отправить комментарий