Несколько подробностей о шаблонах или форк-бомба этапа компиляции

в 18:51, , рубрики: c++, linux, templates, ненормальное программирование, метки: ,

Недавно заинтересовался инстанцированием плюсовых шаблонов. В интернетах втречается термин code bloat. Для с++ это может означать неконтроллируемое увеличение кода генерируемого компилятором. Код увеличивается за счет того что инстанцирование новой функции имеет более высокий приоритет чем преобразование аргументов к более удобному типу. Т.е. template T foo(T a); для int и char — это две разные функции. Получить одну функцию можно либо отказом от шаблонов, либо использованием явного преобразования типов.
Но давайте вывернем проблему наизнанку и попробуем получить из минимума строк кода исполняемый файл максимально возможного размера.
Результат не очень впечатил — у меня получилось всего 53Mb из 60 строк кода. И то лишь для одного из трех опробованных компиляторов и ценой нескольких часов компиляции. Максимальное отношение объем/строки — 2.3МБ/строку для объема 14МБ.
Как и почему так получилось — под катом.

Ресурсы

Один ноутбук c 4Гб памяти,
процессором «Intel® Core(TM) i3-2330M CPU @ 2.20GHz»,
ОС Linux 3.7.3-101.fc17.x86_64
и отключеным swap разделом.
Отключить своп пришлось по тойже причине, по которой в названии поста появилась форк-бомба. При достаточно большом объеме задачи компилятор выедал всю память и начинал активно обмениваться с диском, что намертво и надолго вешало машину.

Версии компиляторов:

  • g++ (GCC) 4.7.2 20120921 (Red Hat 4.7.2-2)
  • Intel® C++ Intel® 64 Compiler XE for applications running on Intel® 64, Version 13.1.1.163 Build 20130313
  • clang version 3.3 (trunk 179304)

Длинные массивы

Самый простой способ — организовать массив этапа комиляции и нанизать на него функций. Вот так:

template<long n> inline void nop(){nop<n-1>();asm("nop"); }
template<> inline void nop<0>() {asm("nop");}
 int main(int argc, char ** argv) {
        nop<LVL>();
        return 0;
} 

В итоге должена получиться функция main() заполненная пустыми бесполезными операциям nop.

Размер последовательности nop теоретически определяется глубиной рекурсии. В мане g++ утверждается, что максимальная глубина 17 и 1024 для с++11.

Цитата из мана

-ftemplate-depth=n
Set the maximum instantiation depth for template classes to n. A limit on the template instantiation depth is needed
to detect endless recursions during template class instantiation. ANSI/ISO C++ conforming programs must not rely on a
maximum depth greater than 17 (changed to 1024 in C++11). The default value is 900, as the compiler can run out of
stack space before hitting 1024 in some situations.

В стандартах конкретных чисел я не нашел. Обнаружился лишь пункт, что максимальная глубина рекурсии определяется реализацией:
4.7.1.14 для с++03
4.7.1.15 для c++11
Что и подтвердилось на опыте. Для достаточно большого LVL компиляторы вылетали. Удивил clang++ который вылетал на 213, в отличие от g++ и icpc достигающих 217.

Сборка осуществлялась командами:

 clang++  -DLVL=$(( 2**$n)) -o list$n ./list.cc -ftemplate-depth=3000000 -O$x
     g++  -DLVL=$(( 2**$n)) -o list$n ./list.cc -ftemplate-depth=3000000 -O$x
    icpc  -DLVL=$(( 2**$n)) -o list$n ./list.cc -O$x

в рамках

сценария сборки

#/bin/bash
MIN=0
MAX=19
for i in 2;
do
        mkdir -p attempt$i
        cd attempt$i
        for CXX in clang++ g++ icpc
        do
                mkdir -p $CXX
                cd $CXX
                for O in O0 O1 O2 O3 Os 
                do
                        mkdir -p $O
                        cd $O
                        (while :;do free -m |grep Mem|awk '{print $3}' >> ./memory;sleep 1;done)&
                        TIME=$!
                        for i in $(seq $MIN $MAX)
                        do
 #                             CMD="$CXX -$O ../../../fbomb.cc -DLVL=$i -o fbomb$i" #-save-temps=obj "
                                CMD="$CXX -$O ../../../list.cc -DLVL=$(( 2 ** $i)) -o list$i  -ftemplate-depth=3000000" #-save-temps=obj "
                                echo $CMD
                                $CMD    
                                sleep 5
                                sleep 5
                        done
                        kill -9 $TIME
                        cd ..
                done
                cd ..
        done
        cd ..
done

Собирались бинарники последовательно и долго. Для каждого компилятора, для каждого уровня оптимизации. Результаты сборки показаны в виде графиков. Четыре столбца изображений:

  1. Зависимость используемой памяти от времени. Эти графики даны скорее для справки, т.к. во время компиляции работали ещё Х-сервер и браузер, однако основные тенденции видны.
  2. Размер бинарного файла. Максимальный получился — 14МБ
  3. Количество символов. Ключевое слово inline — лишь рекомедация компилятору, поэтому при больших N. Nop-ы группируются в обычные функции. Подсчитаны они так:
    nm --demangle ./list$i|grep nop|wc -l
  4. Количество честных nop-ов. Подсчитанно из дизассеблера:
    objdump -d ./list$i|grep 'nop$'|wc -l

Картинки

Сравнение разных уровней оптимизации для одинаковых компиляторов

Потребление памяти во время компиляции Размер исполняемого файла Количество символов в исполняемом файле Количество операций nop
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Сравнение разных компиляторов при различных уровнях оптимизации

Потребление памяти во время компиляции Размер исполняемого файла Количество символов в исполняемом файле Количество операций nop
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции

Файл максимального размера получился 14МБ для 217 компилятора icpc. Для g++ — 12 МБ. Оба при O0. Уровен оптимизации O0 не выполняет inline подстановку, поэтому количество nop<long> символов совпадает с количеством nop операций.

Высокие деревья

Для линейной структуры данных размер генерируемого файла ограничивается, как минимум максимальной глубиной рекурсии принятой по умолчанию(256 для clang++, 900 для g++). Чтобы обойти его можно попробовать создать дерево. Максимальная теоритическая глубина дерева этапа компиляции — (sizeof(long)-1) == 63. А 264 байт переполнит любой диск. Практический предел много меньше.
Используя дерево, мы не выходим за пределы максимальной глубины рекурсии.
Исходный код занимает 19 строк и выглядит так:

#ifndef LVL
#       define LVL 3
#endif
long x = 0;
template<int N=LVL, long I=0> 
struct foo{
        inline static double bar(double m)
                {x++;return foo<N-1,I>::bar(m) + foo<N-1,((1<<(LVL-N))|I)>::bar(m) + I;};
};
template<long I>
struct foo<0,I>{
        inline static double bar(double m) {x++; return m;} 
};
#include <iostream>
int main(int argc, char **argv){
        double ret = foo<>::bar(argc);
        std::cout << x << " " << ret << std::endl;
        return int(ret);
}

В итоге получается дерево, каждый узел которого имеет собственный тип. Каждый тип характеризуется парой чисел:

  • N — номер уровня;
  • I — номер узла на уровне.

Каждый узел внутри одного уровня пронумерован от 0 до N.

Баловаться с nop-ами тут я не стал, использовал сложение. Глобальная long x — использвалась для контроля правильности сборки. В результате возвращается 2LVL+1.

Сборка осуществлялась командами:

 clang++  -DLVL=$n -o fbomb$n ./fbomb.cc  -O$x
     g++  -DLVL=$n -o fbomb$n ./fbomb.cc  -O$x
    icpc  -DLVL=$n -o fbomb$n ./fbomb.cc -O$x

в том же сценарии что приведен выше.
Максимальный LVL получился равным 18 для clang++. Для g++ и icpc — 16, причем не зависимо от того была ли указана опция --std=c++11 или нет. Компиляторам не хватало памяти.

Картинки для дерева

Потребление памяти во время компиляции Размер исполняемого файла Количество символов в исполняемом файле
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Потребление памяти во время компиляции Размер исполняемого файла Количество символов в исполняемом файле
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции
Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции Несколько подробностей о шаблонах или форк бомба этапа компиляции

Максимальный размер файла:

  • 43МБ для icpc -O0 -DLVL=17;
  • 42МБ для clang++ -O0 -DLVL=17;
  • 22MB для g++ -O0 -DLVL=16.

Явное инстанцирование

43МБ — не так уж и мало, но можно ли сделать файл ещё больше при заданном объеме оперативной памяти? Оказалось можно, но только одним компилятором из трех — icpc. Для этого нужно использовать внешние шаблоны и явное инстанцирование.
Модифицируем немного исходный код, чтобы все параметры шаблона указывались при его описании. Разделим исходный код на три файла — описание шаблона, main функция и частичное инстанцирование поддеревьев:

fbomb.hh

extern long x;

template<int L=LVL, int N=L, long I=0>
struct foo{
        inline static double bar(double m)
                {x++;return foo<L,N-1,I>::bar(m) + foo<L,N-1,((1<<(L-N))|I)>::bar(m) + I;};
};
template<int L, long I>
struct foo<L,0,I>{
        inline static double bar(double m) {x++; return m;} 
};

main.cc


#include "fbomb.hh"
//for i in $(seq 0 13);do echo "extern template struct foo<LVL,L,$i>;";done
#define L   (LVL-5)
extern template struct foo<LVL,L,0>;
extern template struct foo<LVL,L,1>;
extern template struct foo<LVL,L,2>;
extern template struct foo<LVL,L,3>;
...
extern template struct foo<LVL,L,30>;
extern template struct foo<LVL,L,31>;

#include <iostream>
long x = 0;
int main(int argc, char **argv){
        double ret = foo<LVL>::bar(argc);
        std::cout << x << " " << ret << std::endl;
        return int(ret);
}

part.cc

template class foo<LVL, _L, _I>;

Оказалось, что g++ -c main.cc -DLVL=21 требует вылетает по недостатку памяти также как при полном инстанцировании независимо от версии стандарта. Такая же ситуация для clang++. Icpc компилирует main.cc менее чем за секунду. Однако компиляция поддеревьев заняла более 4 часов времени:

for i in $(seq 0 31);do
    echo -n "$i:";date;
    icpc -O2 -c ./part.cc -o part21_16_$i.o -DLVL=21 -D_L=16 -D_I=$i;sleep 10;
done
Потребление памяти во время компиляции поддеревьев

Несколько подробностей о шаблонах или форк бомба этапа компиляции

Линковка заняла меньше минуты. В результате получился файл размером 53МБ. Это файл собирался с -O2. -O0 дало бы больший размер, но пересобирать это несколько раз я не стал из-за времени и бессмысленности результата.

Самое большое отношение объем/количество строк = 2.3Мб/cтроку получилось для массива из первой части (icpc -O0 list.cc)
Метрика конечно шутошная, но забавная. 2.3 — максимум что получилось. Буду рад узнать если кто-нибудь получит большее отношение.

Удачи нам всем.

Автор: korisk

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js