- PVSM.RU - https://www.pvsm.ru -
Я только что закончил серию изменений в коде браузера Chrome, которая уменьшила размер его бинарника под Windows примерно на 1 мегабайт, перенесла около 500 КB из read/write сегмента в read-only, а также уменьшила потребление оперативной памяти в общем примерно на 200 KB на каждый процесс Chrome. Удивительное заключается в том, что конкретно данная серия изменений состояла исключительно из удаления и добавления ключевого слова const в некоторых местах кода. Да, компиляторы — странные.
Эта задача возникла, когда я писал документацию [1] для некоторых утилит, которые я использую для исследования регрессий кода, связанных с увеличением размера скомпилированных бинарников под Windows. Я запустил утилиту, скопировал в документацию её вывод и начал его описывать, когда заметил нечто странное: несколько больших глобальных объектов, которые согласно архитектуре должны были быть константными, почему-то находились в сегменте read/write данных. Сокращённая версия того вывода утилиты показана ниже:
Большинство исполняемых форматов имеют как минимум два сегмента данных — один для read/write объектов и ещё один для read-only. Если у вас есть константные данные, такие, например, как kBrotliDictionary [2], то их будет логично поместить в read-only сегмент, который является сегментом «2» в бинарнике Chrome под Windows. Однако некоторые константные данные, такие как unigram_table, device::UsbIds::vendors_ и blink::serializedCharacterData были в секции «3», то есть в read/write сегменте.
Расположение данных в read-only сегменте даёт несколько преимуществ. Это защищает данные от случайного повреждения, а также позволяет использовать их более эффективно. read-only страницы гарантировано будут использоваться совместно всеми процессами, которые загружают данную DLL (а в случае Chrome мы всегда имеем несколько процессов). Кроме того, в некоторых случаях (хотя, наверное, не в этих) компилятор может использовать константы непосредственно в коде.
Страницы в read/write сегменте могут также использоваться совместно, но это не гарантируется. Они все по-умолчанию созданы с флагом "сделать копию при необходимости изменений [3]", что означает их общее использование лишь до первой операции записи, которая приведёт к копировании страницы в личную память процесса. Таким образом, если глобальная переменная будет инициализирована на рантайме — это автоматически сделает её недоступной для общего использования всеми процессами. Кроме того, даже если глобальная переменная всего лишь находится на той же странице памяти, что и другая копируемая при записи — это тоже делает её недоступной для общего использования — всё, знаете ли, происходит с гранулярностью размера страницы (4 KiB).
Приватные данные используют больше памяти, поскольку требуют их отдельной копии в каждом процессе. Кроме того, они более дороги и потому, что требуют места для свопа (чего не нужно для константных данных, ведь их можно при необходимости прочесть из образа бинарника процесса). Это приводит к дорогим операциям записичтения на HDD, которые, к тому же, в общем случае будут по рандомным адресам (ещё медленнее).
По всем этим причинам read-only страницы значительно более предпочтительны во всех случаях, когда это только возможно.
Таким образом, когда моя утилита ShowGlobals [4] показала, что blink::serializedCharacterData был в сегменте read/write данных, а дальнейшее исследование подтвердило, что данный массив никогда не меняется, я добавил [5] к его объявлению модификатор const, что логичным образом перенесло его в сегмент read-only данных. Очень просто. Подобные изменения всегда хорошая идея, но не всегда легко понять, насколько именно. Поскольку мы никогда не меняем данный массив, он, вполне возможно, будет создан в памяти лишь в одном экземпляре и использован всеми процессами Chrome. Но более вероятно, что его конец попадёт на одну страницу с другим объектом, который, возможно, будет меняться и таким образом приведёт к созданию копии страницы памяти (совместно с копией хвоста нашего массива). Таким образом мы потеряем 7748 или 3652 байт (размер массива минус одна или две страницы памяти в середине, которые гарантированно будут общими). Подобные изменения помогут (ну или по крайней мере не помешают) на всех платформах, со всеми компиляторами.
Явное объявление вашего константного массива с модификатором const — это хорошая идея, вам следует делать это. Но одной лишь рассказанной выше информации не будет достаточно для понимания всей картины. И здесь мы вступаем на неизведанную территорию…
Следующий массив, который я исследовал, был unigram_table. Это был странный случай, поскольку он инициализировался исключительно константными данными с помощью синтаксиса инициализации структур/массивов и был помечен модификатором const — но всё же почему-то находился в сегменте read/write данных. Это со всех сторон выглядело какой-то причудой компилятора VC++, так что я воспользовался своей же инструкцией [6] по минимизации необходимого для воспроизведения бага кода и отправил багрепорт в Microsoft. Я скопировал типы и объявление массива в отдельный проект и продолжал уменьшать его, на каждом шагу проверяя расположение массива в read/write сегменте данных. В конце концов я дошел до минималистичного кода, который поместился бы в твит [7]:
const struct T {const int b[999]; } a[] = {{{}}}; int main() {return(size_t)a;}
Если вы скомпилируете этот код и запустите ShowGlobals [4] на полученном PDB, утилита покажет, что «а» находится в секции «3», несмотря на объявление с модификатором const. Вот конкретные шаги по сборке и тестированию кода:
> “%VS140COMNTOOLS%....VCvcvarsall.bat”
> cl /Zi constbug.cpp
/out:constbug.exe
> ShowGlobals.exe constbug.pdb
Size Section Symbol name
3996 3 a
После уменьшения моего примера до менее 140 символов стало очень просто найти причину. С компиляторами VC++ (2010, 2015, 2017 RC) получается так, что если у вас есть класс/структура с константным членом данных, то любой глобальный объект данного типа попадёт в read/write сегмент данных. Jonathan Caves объяснил в своём комментарии к моему багрепорту [8], что это происходит потому, что тип получает сгенерированный компилятором удалённый конструктор по умолчанию (имеет смысл), что сбивает с толку компилятор VC++, который ошибочно определяет данный класс, как требующий динамической инициализации.
Таким образом, проблема в данном случае в модификаторе const, стоящем возле члена данных «b». Как только я удалил этот const — весь массив попал в read-only память (весьма иронично, правда?). Поскольку весь объект так или иначе является константным, удаление модификатора const у одного из его членов данных нисколько не уменьшает безопасность, а для компилятора VC++ по факту увеличивает её.
Я рассчитываю, что команда разработчиков VC++ исправит данный баг к выходу VS 2017 — в этом случае код можно было бы и не исправлять — но я не хочу ждать так долго. И я начал убирать модификаторы const в тех местах, где это вызывало подобные проблемы. Процесс был достаточно тривиальным — я просто продолжал просматривать список глобальных переменных в read/write сегменте данных и относить их к одной из следующих категорий:
Так я шел по коду Chrome, добавляя и убирая const в подходящих местах. В большинстве случаев мои изменения, как и планировалось, приводили к перемещению данных из read/write сегмента в read-only сегмент. Но в двух случаях эти изменения сделали также кое-что ещё — уменьшили размер секций .text и .reloc. Это было просто отлично, даже слишком хорошо для того, чтобы быть правдой. Я предполагаю, что VC++ генерировал код для инициализации некоторых из этих массивов — и достаточно много кода.
Самым интересным изменением было удаление трёх const из определения структуры UnigramEntry. Это перенесло в read-only сегмент 53064 байт, а также уменьшило размер chrome.dll и chrome_child.dll на 364500 байт. Из этого следует, что компилятор VC++ молчаливо создавал код инициализации, который занимал по 7 байт на инициализацию каждого байта unigram_table. Такого попросту не могло быть. Это было слишком далеко за рамками моих ожиданий, так что я запустил Chrome под отладчиком Visual Studio и установил брейкпоинт на изменение данных в в конце массива unigram_table. Visual Studio предсказуемо остановила выполнение программы в инициализаторе. Ниже я приведу (немного вычищенный) ассемблерный код инициализатора (я заменил «unigram_table» на «u» для повышения читабельности):
55 push ebp
8B EC mov ebp,esp
83 25 78 91 43 12 00 and dword [u],0
83 25 7C 91 43 12 00 and dword [u+4],0
83 25 80 91 43 12 00 and dword [u+8],0
83 25 84 91 43 12 00 and dword [u+0Ch],0
C6 05 88 91 43 12 4D mov byte [u+10h],4Dh
C6 05 89 91 43 12 CF mov byte [u+11h],0CFh
C6 05 8A 91 43 12 1D mov byte [u+12h],1Dh
C6 05 8B 91 43 12 1B mov byte [u+13h],1Bh
C7 05 8C 91 43 12 FF 00 00 00 mov dword [u+14h],0FFh
C6 05 90 91 43 12 00 mov byte [u+18h],0
C6 05 91 91 43 12 00 mov byte [u+19h],0
C6 05 92 91 43 12 00 mov byte [u+1Ah],0
C6 05 93 91 43 12 00 mov byte [u+1Bh],0
… 52,040 lines deleted…
c6 05 02 6e 0b 12 6c mov byte [u+cf42h],6Ch
c6 05 03 6e 0b 12 6e mov byte [u+cf43h],6Eh
c6 05 04 6e 0b 12 a2 mov byte [u+cf44h],0A2h
c6 05 05 6e 0b 12 c2 mov byte [u+cf45h],0C2h
c6 05 06 6e 0b 12 80 mov byte [u+cf46h],80h
c6 05 07 6e 0b 12 c4 mov byte [u+cf47h],0C4h
5d pop ebp
c3 ret
Числа в 16-ричной системе счисления слева — это машинные коды команд, а текст справа — это их ассемблерное представление. После некоторого пролога мы видим код, заполняющий массив… по одному байту… используя 7 инструкций. Ну, это всё объясняет.
Общеизвестно, что современные оптимизирующие компиляторы могут сгенерировать код, который будет так же хорош, как написанный человеком (а чаще всего ещё лучше). И всё же — иногда они такой код не пишут. В данной конкретной функции есть множество моментов, который могли бы быть сделаны лучше:
Ну, в общем, вы поняли суть. Эта функция могла бы быть раза в 4 меньше, а также полностью отсутствовать. Она и пропала после убирания трёх модификаторов const (изменения уже доступны в Chrome Canary [9]), а вместе с ней пропали лишние ~364500 байт кода и ~105000 байт в секции .reloc, и это произошло как в chrome.dll, так и в chrome_child.dll. Массив раньше был в .BSS [10] (инициализируемая нулями часть read/write сегмента), где он не занимал никакого места на диске, а переместился в read-only сегмент, где стал занимать 53064 байт, поэтому общая экономия места на диске составила 416000 байт на каждую DLL.
И, что ещё более важно, большинство глобальных переменных, затронутых данными [11] изменениями [12], перешли из приватной памяти каждого процесса в разделяемую общую память, что дало экономию оперативной памяти около 200 KB на каждый процесс.
Я начал с самых больших и часто используемых объектов и типов для того, чтобы получить хороший и сразу видимый результат. Я быстро уменьшил размер read/write сегмента примерно на 250 KB, переместив около 1500 глобальных переменных в read-only сегмент. Это дело, знаете ли, затягивает (что? у кого тут обсессивно-компульсивное расстройство? у меня? понятия не имею, о чём вы). Но мне удалось остановиться на каком-то этапе, хотя я точно знаю, что в коде всё ещё остались сотни более мелких глобальных переменных, которые можно было бы исправить аналогичным образом. В какой-то момент мне показалось, что затрачиваемые мною усилия больше не стоят достигаемого выигрыша в несколько байт памяти и пора двигаться куда-то дальше. Но, если вы всегда мечтали что-нибудь закоммитить в код Chrome, не стесняйтесь пойти вышеуказанным путём. Ради примера вы можете посмотреть на несколько проделанных мною изменений:
Изменения, удаляющие const:
Изменения, добавляющие const:
Если вы хотите подебажить Chrome и посмотреть на код инициализатора unigram_table перед тем, как он пропадёт при следующем релизе Chrome — вам не нужно быть крутым разработчиком Chrome. Начните с выполнения вот этих двух команд:
> “%VS140COMNTOOLS%....VCvcvarsall.bat”
> devenv /debugexe chrome.exe
Убедитесь, что вы добавили в настройки отладчика путь к символьному серверу Chrome (вот по этой инструкции [21]) и установили брейкпоинт вот на этот символ:
`dynamic initializer for 'unigram_table''
Убедитесь, что у вас нет запущенного в данный момент Chrome и запустите его из-под Visual Studio. Visual Studio загрузит символы Chrome (магия символьных серверов! [22]) и установит брейкпоинт на инициализатор (если он всё-ещё существует). Ничего сложного. Вы можете переключиться в режим ассемблерного кода (Ctrl+F11). Если вы хотите видеть исходный код — просто включите использование сервера исходных кодов [23] в настройках отладчика Visual Studio.
Автор: Инфопульс Украина
Источник [24]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/245378
Ссылки в тексте:
[1] писал документацию: https://www.chromium.org/developers/windows-binary-sizes
[2] kBrotliDictionary: https://cs.chromium.org/chromium/src/third_party/brotli/common/dictionary.c?l=24
[3] сделать копию при необходимости изменений: https://en.wikipedia.org/wiki/Copy-on-write
[4] ShowGlobals: https://cs.chromium.org/chromium/src/tools/win/ShowGlobals/
[5] добавил: https://codereview.chromium.org/2608823002
[6] своей же инструкцией: https://randomascii.wordpress.com/2013/10/14/how-to-report-a-vc-code-gen-bug/
[7] твит: https://twitter.com/BruceDawson0xB/status/814251241417031680
[8] багрепорту: http://wordpress.redirectingat.com/?id=725X1342&site=randomascii.wordpress.com&xs=1&isjs=1&url=https%3A%2F%2Fconnect.microsoft.com%2FVisualStudio%2Ffeedback%2Fdetails%2F3117602&xguid=4242e57c1247cd811377d5283879c3dc&xuuid=293bf7082b710cccee28deebbd209894&xsessid=dd6b9e5d517dfe94a370913bf4d302de&xcreo=0&xed=0&sref=https%3A%2F%2Frandomascii.wordpress.com%2F2017%2F01%2F08%2Fadd-a-const-here-delete-a-const-there%2F&xtz=-120
[9] Chrome Canary: https://www.google.com/chrome/browser/canary.html
[10] .BSS: https://en.wikipedia.org/wiki/.bss
[11] данными: https://chromium.googlesource.com/external/github.com/google/compact_enc_det.git/+/9a5abb86e339beca2ad7f375934a38727e810f45
[12] изменениями: https://codereview.chromium.org/2607893002/
[13] Пять удалений const для перемещения 12500 байт в read-only сегмент: https://codereview.chromium.org/2608763002/
[14] Удаление const для перемещения 6800 байт в read-only сегмент: https://codereview.chromium.org/2607183002
[15] Четыре удаления const для перемещения 2500 байт в read-only сегмент: https://codereview.chromium.org/2607993002/
[16] Шесть удалений const для перемещения 960 байт в read-only сегмент: https://codereview.chromium.org/2605393002/
[17] Пять удалений const для перемещения 250 байт в read-only сегмент: https://codereview.chromium.org/2613723005
[18] Добавление const для перемещения 12864 байт в read-only сегмент данных: https://codereview.chromium.org/2608643002/
[19] Добавление двух const для перемещения 3000 байт в read-only сегмент данных: https://skia-review.googlesource.com/c/6424/
[20] Добавление const для перемещения 396 байт в read-only сегмент данных: https://github.com/xiph/flac/issues/26
[21] этой инструкции: https://www.chromium.org/developers/how-tos/debugging-on-windows
[22] магия символьных серверов!: https://randomascii.wordpress.com/2013/03/09/symbols-the-microsoft-way/
[23] сервера исходных кодов: https://randomascii.wordpress.com/2011/11/11/source-indexing-is-underused-awesomeness/
[24] Источник: https://habrahabr.ru/post/322320/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.