Понятно, что предыдущую запись я написал не просто так, а с мыслью когда-нибудь такой тест реализовать. Вот и реализовал.
Краткая предыстория. Когда давно я с сыном соревновался, смогу ли я написать сжатие Хаффманом лучше, чем он. Хотя детали уже не помню, может все было совсем не так. Недавно просматривая свой старый код, решил портировать его под 64 бита и немного оптимизировать.
Вот что получилось (время операции указано в секундах, программа компилировалась на Delphi XE5):
| Операция | 32 bit | 64 bit | Потери производительности |
| Сжатие | 0.62 | 0.92 | 47.02% |
| Восстановление | 2.81 | 4.30 | 53.16% |
| Замедление | 4.515512 | 4.704038 |
В последней строчке показано, во сколько раз распаковка данных медленней, чем сжатие. Просто для информации. Сжимался 8-битный файл изображения в формате tif размером 57 Мбайт, после сжатия размер файла составил 46 Мбайт. В данном случае тестировалась именно сама операция сжатия, без учета построения дерева Хаффмана.
Как видите 64-битная архитектура для данного примера оказалась существенно медленнее. Почему так? На мой взгляд, причина именно в лимитировании скорости работы памяти. И дело тут не только в использовании ссылок, но и в вызове процедур или функций. Каждый такой вызов приводит как минимум к двум обращениям к памяти, а обычно существенно больше.
В своем последнем курсе по структурам и алгоритмам я обращал на это внимание. При оптимизации по скорости работы нужно избегать использования функции с коротким телом именно из-за этих дополнительных обращений к памяти. В случае же 64-битной архитектуры эти обращения потребует в два раза больше ресурсов при прочих равных.
Кстати, еще один интересный момент: много регистров - тоже палка о двух концах. Ведь при вызове процедуры использованные регистры надо сохранить в стеке, а потом восстановить их обратно. И чем больше регистров, тем больше издержки на вызов функции.
На самом деле причины в низкой производительности могут быть и другие, выше я привел только свои предположения. Как думаете вы, с чем связаны такие результаты 64-битного кода?
Если мое предположение верно, то на 64-битной архитектуре лучше в самом деле пользоваться массивами, даже при работе с древовидными структурами. Дело в том, что при обращении к массиву достаточно загрузить одну ссылку на его начало, а адреса ячеек массива очень эффективно рассчитываются современными процессорами без дополнительных обращений к памяти.
Что касается конкретно этой задачи, то, возможно, здесь стоило воспользоваться каноническим деревом Хаффмана, что позволяет не использовать дерево в явном виде при декодировании. В результате одна проверка указателя и один переход по ссылке заменяется одной проверкой элемента массива и одной арифметической операцией. Честно говоря, реализовывать такой код было лом, так как я исходил из предположения, что оба подхода скорее всего эквиваленты по производительности. Возможно, я ошибался.
Так что не нужно думать, что 64-битная программа будет быстрее 32-битной. Понятно, что у 64-разрядного процессора есть много разных преимуществ, но только реализуются они не так уж и часто. Это задачи, обрабатывающие очень большие объемы данных, более 4 Гбайт, а также вычисления с большой точностью, например, шифрация данных. В обыденной жизни такие задачи встречаются не так уж и часто.
Еще один интересный момент в этом небольшом исследовании связан с тем, что распаковка происходит медленнее, чем сжатие. Происходит это потому, что при распаковке заранее не известно, сколькими битами закодирован символ, поэтому считывать данные приходится побитно. При сжатии же код символа известен заранее и записывается он в выходной поток одной операцией.
Неожиданные результаты. По идеи в этом алгоритме 32-bit как раз не должны выиграть:
ОтветитьУдалить1. Дерево маленькое, максимум 512 узлов (если сжимать по байтам). Оно все должно быть в кэше, и процессору не важно что читать: 32 байта в 32 битный регистр или 64 в 64 битный.
2. Основное чтение/запись из памяти - это данные, их размер не зависит от архитектуры.
3. Логика простая и более-менее умный компилятор должен все функции по максимуму заинлайнить.
Я если честно не в курсе, как там дела у Delphi. После университета не пользовался ни разу. Вполне возможно что 64-bit компилятор менее умный. Вот например человек жалуется на него http://blog.digitaltundra.com/?p=296 , а вот ещё: http://stackoverflow.com/questions/11260441/delphi-xe2-64-bit-extremely-slow-runtime-performance-on-string-routines
Не знаю на сколько это правда.
Ага. Я тоже не ожидал такой разницы :)
Удалить1. Дерево-то в кэше. Но из кэша его надо передать в процессор. А шина между процессором и кэшем не резиновая. В случае 64 бит при работе с деревом надо передавать как минимум в 2 раза больше данных, так как оно на ссылках построено.
2. Не в этом случае. При чтении каждого бита данныъ нужно выполнять операции с деревом, соответственно, читаем 1 бит данных и выполняем операции с деревом. На 1 бит данных - 4 (или 8) байт ссылки. Также декодирование выполняется путем вызова процедуры, а это дополнительные операции со стеком, что тоже загружает шину памяти.
3. Ну, насчет этого пункта не знаю что сказать. Обычно инлайнятся очень простые функции, а тут они все же посложнее немного.
У меня тоже были мысли по поводу эффективности компилятора. Я немного сгененированный код посмотрел, он, конечно, существенно разный под 32 бита и под 64. Но я не могу сказать, что под 64 бита он вообще не оптимизированный. Сейчас не буду подробно тут про это говорить, это отдельная тема.
Хочу сказать, что в Делфи настроек оптимизации не много, но тем не менее, одним ключиком все не решается. Это я по поводу статей, которые ты привел в комментарии.
Ну и очевидно, на мой взгляд, что, по крайней мере, первые версии компилятора под 64 бита были не самые оптимальные. Все же 32-битный компилятор отлаживался больше 10 лет. Когда-нибудь и 64 бита будут под Delphi неплохо компилироваться. :)