- PVSM.RU - https://www.pvsm.ru -

Там добавим const, отсюда удалим const…

Я только что закончил серию изменений в коде браузера Chrome, которая уменьшила размер его бинарника под Windows примерно на 1 мегабайт, перенесла около 500 КB из read/write сегмента в read-only, а также уменьшила потребление оперативной памяти в общем примерно на 200 KB на каждый процесс Chrome. Удивительное заключается в том, что конкретно данная серия изменений состояла исключительно из удаления и добавления ключевого слова const в некоторых местах кода. Да, компиляторы — странные.

Эта задача возникла, когда я писал документацию [1] для некоторых утилит, которые я использую для исследования регрессий кода, связанных с увеличением размера скомпилированных бинарников под Windows. Я запустил утилиту, скопировал в документацию её вывод и начал его описывать, когда заметил нечто странное: несколько больших глобальных объектов, которые согласно архитектуре должны были быть константными, почему-то находились в сегменте read/write данных. Сокращённая версия того вывода утилиты показана ниже:

image

Большинство исполняемых форматов имеют как минимум два сегмента данных — один для read/write объектов и ещё один для read-only. Если у вас есть константные данные, такие, например, как kBrotliDictionary [2], то их будет логично поместить в read-only сегмент, который является сегментом «2» в бинарнике Chrome под Windows. Однако некоторые константные данные, такие как unigram_table, device::UsbIds::vendors_ и blink::serializedCharacterData были в секции «3», то есть в read/write сегменте.

image

Расположение данных в read-only сегменте даёт несколько преимуществ. Это защищает данные от случайного повреждения, а также позволяет использовать их более эффективно. read-only страницы гарантировано будут использоваться совместно всеми процессами, которые загружают данную DLL (а в случае Chrome мы всегда имеем несколько процессов). Кроме того, в некоторых случаях (хотя, наверное, не в этих) компилятор может использовать константы непосредственно в коде.

Страницы в read/write сегменте могут также использоваться совместно, но это не гарантируется. Они все по-умолчанию созданы с флагом "сделать копию при необходимости изменений [3]", что означает их общее использование лишь до первой операции записи, которая приведёт к копировании страницы в личную память процесса. Таким образом, если глобальная переменная будет инициализирована на рантайме — это автоматически сделает её недоступной для общего использования всеми процессами. Кроме того, даже если глобальная переменная всего лишь находится на той же странице памяти, что и другая копируемая при записи — это тоже делает её недоступной для общего использования — всё, знаете ли, происходит с гранулярностью размера страницы (4 KiB).

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

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

Добавить const — это хорошо

Таким образом, когда моя утилита ShowGlobals [4] показала, что blink::serializedCharacterData был в сегменте read/write данных, а дальнейшее исследование подтвердило, что данный массив никогда не меняется, я добавил [5] к его объявлению модификатор const, что логичным образом перенесло его в сегмент read-only данных. Очень просто. Подобные изменения всегда хорошая идея, но не всегда легко понять, насколько именно. Поскольку мы никогда не меняем данный массив, он, вполне возможно, будет создан в памяти лишь в одном экземпляре и использован всеми процессами Chrome. Но более вероятно, что его конец попадёт на одну страницу с другим объектом, который, возможно, будет меняться и таким образом приведёт к созданию копии страницы памяти (совместно с копией хвоста нашего массива). Таким образом мы потеряем 7748 или 3652 байт (размер массива минус одна или две страницы памяти в середине, которые гарантированно будут общими). Подобные изменения помогут (ну или по крайней мере не помешают) на всех платформах, со всеми компиляторами.

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

Иногда убрать 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 сегменте данных и относить их к одной из следующих категорий:

  • Те, значения которых меняется — оставляем, как есть
  • Не меняются и не имеют модификатора const — добавляем его
  • Не меняются и имеют проблемный член данных с модификатором const — убираем его

Это было правда забавно

Так я шел по коду 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 инструкций. Ну, это всё объясняет.

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

  • Она могла бы вообще не существовать. Массив инициализируется простым синтаксисом инициализации массивов в С и, если бы не вышеописанный баг в компиляторе VC++, код инициализатора вообще не нужно было бы генерировать (как это и происходит на других платформах).
  • Запись нулей можно было бы пропустить. Данный массив — это глобальная переменная, которая инициализируется лишь раз при запуске программы, а в этот момент вся память гарантированно заполнена нулями, так что записывать нули поверх нулей — бессмысленная работа.
  • Данные можно было бы записывать по 4 байта за раз, а не по одному
  • Адресс массива можно было бы загрузить в регистр и использовать его оттуда, вместо того, чтобы указывать его в каждой инструкции. Это сделало бы инструкции меньше, а также сохранило бы 2 байта на инструкцию релокации данных, найденных в .reloc сегменте.

Ну, в общем, вы поняли суть. Эта функция могла бы быть раза в 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