Hi,
Не могу не поделиться прекрасным. Итак исходные данные -- ваяем компилятор GCC (бэкенд к нему). От пользователя пришла проблема, схематично изображённая на рисунке ниже:
есть код A, в котором функция foo() проинлайнена в bar(). Присоединяем к нему код B, который ни к одной из функций не имеет никакого отношения и foo() перестаёт инлайниться. И код A и код B собирались с LTO. Была задача определить почему так произошло, виноват в этом наш бэкенд или это фишка компилятора как такового.
Расковыряв механизм IPA в GCC, я обнаружил там обычную priority queue, куда складываются все функции-кандидаты на инлайн с эвристическими приоритетами. При этом каждый ltrans-модуль имеет ограничение на то, на сколько он может вырасти при инлайне. Как только он вырос "вот на столько", инлайн прекращается. Из-за добавленного кода B, функция foo() отодвигалась по очереди слишком далеко и инлайн прекращался раньше чем очередь доходила до неё. Пользователю я порекомендовал подать в командную строку GCC параметр inline-unit-growth побольше чем дефолтный:
--param inline-unit-growth=60
И это решило проблему.
Через некоторое время, где-то два месяца, ко мне обратился другой пользователь, у которого был принципиально другой код, но очень похожая проблема, схематично изображённая на рисунке 2:
Есть код A+B, в котором функция foo() проинлайнена в bar(). Удаляем из него код B, и функция foo перестаёт инлайниться. Код B не имеет никакого отношения ни к foo, ни к bar, он просто лежит рядом. И код A и код B собирались с LTO.
Этому пользователю я предложил то же самое решение (увеличить --param inline-unit-growth по сравнению с дефолтными 60%) и оно ему помогло.
Кто-нибудь попробует догадаться почему?
А ответ крайне прост -- lto в GCC бьёт код на фиксированное количество ltrans модулей. Для кода A+B эти модули получались достаточно большими и в 60% от них попадал инлайн foo. После того как код B убрали, ltrans-модули стали меньше и инлайн foo перестал попадать в ограничение на рост модуля.
---
With best regards, Konstantin