Шаблоны C ++: убедить себя против раздувания кода - PullRequest
14 голосов
/ 27 мая 2010

Я слышал о раздувании кода в контексте шаблонов C ++. Я знаю, что это не так с современными компиляторами C ++. Но я хочу построить пример и убедить себя.

Допустим, у нас есть класс

template< typename T, size_t N >
class Array {
  public:
    T * data();
  private:
    T elems_[ N ];
};

template< typename T, size_t N >
T * Array<T>::data() {
    return elems_;
}

Далее, скажем, types.h содержит

typedef Array< int, 100 > MyArray;

x.cpp содержит

MyArray ArrayX;

и y.cpp содержит

MyArray ArrayY;

Теперь, как я могу убедиться, что кодовое пространство для MyArray::data() одинаково как для ArrayX, так и для ArrayY?

Что еще я должен знать и проверить из этого (или других подобных простых) примеров? Если есть какие-то конкретные советы по g ++, мне это тоже интересно.

PS: Что касается вздутия, меня беспокоит даже малейшее из-за раздувания, так как я пришел из встроенного контекста.


Дополнение: Изменится ли ситуация в любом случае, если классы шаблонов будут созданы явно?

Ответы [ 7 ]

12 голосов
/ 27 мая 2010

Вы задаете неправильный вопрос - любое «раздувание» в вашем примере не имеет ничего общего с шаблонами. (кстати, ответ на ваш вопрос - взять адрес функции-члена в обоих модулях, и вы увидите, что они одинаковы)

Что вы действительно хотите спросить, так это то, что для каждого экземпляра шаблона линейный рост результирующего исполняемого файла? Ответ - нет, компоновщик / оптимизатор будет творить чудеса.

Скомпилируйте исполняемый файл, который создает один тип:

Array< int, 100 > MyArray;

Обратите внимание на размер exe. Теперь сделайте это снова:

Array< int, 100 > MyArray;
Array< int, 99 > MyArray;

И так далее, для 30 или около того разных версий, составление графиков результирующих размеров исполняемых файлов. Если бы шаблоны были такими ужасными, как думают люди, размер exe увеличился бы на фиксированную величину для каждого уникального экземпляра шаблона.

10 голосов
/ 27 мая 2010

В этом конкретном случае вы обнаружите, что g ++ будет стремиться включить аксессор, если у вас есть какая-либо оптимизация. Это право есть некоторые незначительные код раздуваться, хотя это спорно, если накладные расходы на вызов будет меньше.

Однако, один простой способ проверить, что компилируется, - инструмент nm. Если я скомпилирую ваш код с помощью простого main() для упражнения ArrayX::data() и ArrayY::data(), а затем скомпилирую его с помощью -O0, чтобы отключить встраивание, я могу запустить nm -C, чтобы увидеть символы в исполняемом файле:

% nm -C test
0804a040 B ArrayX
0804a1e0 B ArrayY
08049f08 d _DYNAMIC
08049ff4 d _GLOBAL_OFFSET_TABLE_
0804858c R _IO_stdin_used
         w _Jv_RegisterClasses
080484c4 W Array<int, 100u>::data()
08049ef8 d __CTOR_END__
08049ef4 d __CTOR_LIST__
08049f00 D __DTOR_END__
...

Вы увидите, что символ Array<int, 100u>::data() встречается только один раз в конечном исполняемом файле, даже если объектный файл для каждого из двух блоков перевода содержит свою собственную копию. (Инструмент nm также работает с объектными файлами. Вы можете использовать его для проверки того, что x.o и y.o каждый имеет копию Array<int, 100u>::data().)

Если nm не предоставляет достаточно подробностей, вы также можете взглянуть на инструмент objdump. Это очень похоже на nm, но с включенными символами отладки он может даже показать вам такие вещи, как разборка выходного исполняемого файла с смешанными исходными строками.

7 голосов
/ 27 мая 2010

Шаблоны не имеют к этому никакого отношения.

Рассмотрим эту маленькую программу:

хиджра:

class a {
    int foo() { return 42; }
};

b.cpp:

#include "a.h"

void b() {
  a my_a;
  my_a.foo();
}

c.cpp:

#include "a.h"

void c() {
  a my_a;
  my_a.foo();
}

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

«Проблема» с раздуванием кода шаблона - это нечто иное: это если вы создаете много разных экземпляров одного и того же шаблона. Например, используя ваш класс, эта программа может подвергнуться некоторому увеличению кода:

Array< int, 100 > i100;
Array< int, 99 > i99;
Array< long, 100 > l100;
Array< long, 99> l99;

i100.Data();
i99.Data();
l100.Data();
l99.Data();

Строго говоря, компилятору требуется создать 4 различных экземпляра функции Data, по одному для каждого набора параметров шаблона. На практике некоторые (но не все) компиляторы пытаются объединить их вместе, если сгенерированный код идентичен. (В этом случае сборка, сгенерированная для Array< int, 100 > и Array< long, 100 >, будет идентична на многих платформах, и функция также не зависит от размера массива, поэтому варианты 99 и 100 также должны генерировать идентичный код, поэтому умный компилятор объединит экземпляры вместе.

В шаблонах нет магии. Они не загадочно «раздувают» ваш код. Они просто дают вам инструмент, который позволяет вам легко создавать различные типы баджиллионов из одного и того же шаблона. Если вы на самом деле используете все эти типы, он должен сгенерировать код для всех них. Как всегда в C ++, вы платите за то, что используете. Если вы используете Array<long, 100>, Array<int, 100>, Array<unsigned long, 100> и Array<unsigned int, 100>, тогда вы получите четыре разных класса, потому что вы запросили четыре разных класса. Если вы не попросите четыре разных класса, они вам ничего не будут стоить.

4 голосов
/ 27 мая 2010

Лучшей иллюстрацией раздувания кода с шаблонами является использование шаблона для генерации кода, а не переменных. Типичная паника из-за того, что компилятор генерирует код для каждого экземпляра шаблона (трафарета). Это похоже на раздувание кода из-за встроенных функций и методов. Однако современные компиляторы и компоновщики могут выполнять магию для уменьшения размера кода в зависимости от настроек оптимизации.

Например:

template <typename Any_Type>
void Print_Hello(const Any_Type& v)
{
    std::cout << "Hello, your value is:\n"
              << v
              << "\n";
    return;
}

Приведенный выше код лучше всего рассматривать как трафарет. Компилятор сгенерирует код в зависимости от типа, переданного в Print_Hello. Суть в том, что очень мало кода на самом деле зависит от переменной. (Который может быть уменьшен путем выделения кода и данных const.)

Опасение заключается в том, что компилятор будет генерировать код для каждого экземпляра, используя один и тот же тип переменной, создавая, таким образом, повторяющийся код:

int main(void)
{
  int a = 5;
  int b = 6;
  Print_Hello(a); // Instantiation #1
  Print_Hello(b); // Instantiation #2
  return 0;
}

Страх также может быть расширен, когда шаблон (трафарет) создается в разных единицах перевода.

Современные компиляторы и линкеры умны. Умный компилятор распознает вызов функции шаблона и преобразует в какое-то уникальное искаженное имя. Компилятор будет использовать только один экземпляр для каждого вызова. Аналогично перегрузке функций.

Даже если компилятор был неаккуратным и генерировал несколько экземпляров функции (для одного и того же типа), компоновщик распознал бы дубликаты и поместил только один экземпляр в исполняемый файл.

При безукоризненном использовании шаблон функции или метода может добавить дополнительный код. Примерами являются большие функции, которые различаются по типу только в нескольких областях. Они имеют высокое отношение нетипизированного кода к типозависимому коду.

Реализация приведенного выше примера с меньшим количеством раздувания:

void Print_Prompt(void)
{
  std::cout << "Hello, your value is:\n";
  return;
}

template <typename Any_Type>
void Better_Print_Hello(const Any_Type& v)
{
  Print_Prompt();
  std::cout << v << "\n";
  return;
}

Основное отличие состоит в том, что код, который не зависит от типа переменной, был преобразован в новую функцию. Это может показаться нецелесообразным для этого небольшого примера, но оно иллюстрирует концепцию. И концепция заключается в том, чтобы реорганизовать функцию в части, которые зависят от типа переменной, и те, которые не являются. Части, которые являются зависимыми, преобразуются в шаблонные функции.

3 голосов
/ 27 мая 2010

Вот небольшой служебный скрипт, который я использовал, чтобы понять только эти проблемы. Он показывает вам не только, если символ определен несколько раз, но также и размер кода, который занимает каждый символ. Я нашел это чрезвычайно ценным для аудита проблем с размером кода.

Например, вот пример вызова:

$ ~/nmsize src/upb_table.o 
 39.5%     488 upb::TableBase::DoInsert(upb::TableBase::Entry const&)
 57.9%     228 upb::TableBase::InsertBase(upb::TableBase::Entry const&)
 70.8%     159 upb::MurmurHash2(void const*, unsigned long, unsigned int)
 78.0%      89 upb::TableBase::GetEmptyBucket() const
 83.8%      72 vtable for upb::TableBase
 89.1%      65 upb::TableBase::TableBase(unsigned int)
 94.3%      65 upb::TableBase::TableBase(unsigned int)
 95.7%      17 typeinfo name for upb::TableBase
 97.0%      16 typeinfo for upb::TableBase
 98.0%      12 upb::TableBase::~TableBase()
 98.7%       9 upb::TableBase::Swap(upb::TableBase*)
 99.4%       8 upb::TableBase::~TableBase()
100.0%       8 upb::TableBase::~TableBase()
100.0%       0 
100.0%       0 __cxxabiv1::__class_type_info
100.0%       0 
100.0%    1236 TOTAL

В этом случае я запустил его для одного файла .o, но вы также можете запустить его для файла .a или исполняемого файла. Здесь я вижу, что конструкторы и деструкторы испускались дважды или трижды, что является результатом этой ошибки .

Вот сценарий:

#!/usr/bin/env ruby

syms = []
total = 0
IO.popen("nm --demangle -S #{ARGV.join(' ')}").each_line { |line|
  addr, size, scope, name = line.split(' ', 4)
  next unless addr and size and scope and name
  name.chomp!
  addr = addr.to_i(16)
  size = size.to_i(16)
  total += size
  syms << [size, name]
}

syms.sort! { |a,b| b[0] <=> a[0] }

cumulative = 0.0
syms.each { |sym|
  size = sym[0]
  cumulative += size
  printf "%5.1f%%  %6s %s\n", cumulative / total * 100, size.to_s, sym[1]
}

printf "%5.1f%%  %6s %s\n", 100, total, "TOTAL"

Если вы запустите это на своих собственных .a-файлах или исполняемых файлах, вы сможете убедить себя в том, что точно знаете, что происходит с размером вашего кода. Я считаю, что последние версии gcc могут удалять избыточные или бесполезные экземпляры шаблонов во время компоновки, поэтому я рекомендую проанализировать ваши реальные исполняемые файлы.

3 голосов
/ 27 мая 2010

Один из тестов - поместить в data () статическую переменную, увеличивать ее при каждом вызове и сообщать об этом.

Если MyArray :: data () занимает то же самое пространство кода, то вы должны увидеть в нем отчет 1, а затем 2.

Если нет, вы должны просто увидеть 1.

Я запустил его и получил 1, а затем 2, что указывает на то, что он работает с того же набора кода. Чтобы убедиться, что это действительно так, я создал еще один массив с параметром размера 50, и он выкинул 1.

Полный код (с парой настроек и исправлений) ниже:

Array.hpp:

#ifndef ARRAY_HPP
#define ARRAY_HPP
#include <cstdlib>
#include <iostream>

using std::size_t;

template< typename T, size_t N >
class Array {
  public:
    T * data();
  private:
    T elems_[ N ];
};

template< typename T, size_t N >
T * Array<T,N>::data() {
    static int i = 0;
    std::cout << ++i << std::endl;
    return elems_;
}

#endif

types.hpp:

#ifndef TYPES_HPP
#define TYPES_HPP

#include "Array.hpp"

typedef Array< int, 100 > MyArray;
typedef Array< int, 50 > MyArray2;

#endif

x.cpp:

#include "types.hpp"

void x()
{
    MyArray arrayX;
    arrayX.data();
}

y.cpp:

#include "types.hpp"

void y()
{
    MyArray arrayY;
    arrayY.data();
    MyArray2 arrayY2;
    arrayY2.data();
}

main.cpp:

void x();
void y();

int main()
{
    x();
    y();
    return 0;
}
0 голосов
/ 27 мая 2010

Сгенерированный код будет точно таким же, так как код в обоих файлах абсолютно одинаков.Вы можете разобрать код, чтобы проверить его, если хотите.

...