Шаблоны C++: Убеждение сам против чрезмерного увеличения размера кода

Я услышал о чрезмерных увеличениях размера кода в контексте шаблонов 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: Относительно чрезмерного увеличения размера я заинтересован даже для малейшего из чрезмерных увеличений размера, так как я происхожу из встроенного контекста.


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

14
задан Arun 27 May 2010 в 17:29
поделиться

7 ответов

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

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

Скомпилируйте exe, создающий один тип:

Array< int, 100 > MyArray;

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

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

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

12
ответ дан 1 December 2019 в 07:12
поделиться

В этом конкретном случае вы обнаружите, что 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::data() встречается только один раз в конечном исполняемом файле, хотя объектный файл для каждого из двух блоков трансляции содержит свою собственную копию. (Инструмент nm также работает с объектными файлами. С его помощью можно проверить, что x.o и y.o содержат по копии Array::data().)

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

10
ответ дан 1 December 2019 в 07:12
поделиться

Шаблоны тут ни при чем.

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

a.h:

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 и Array , будет идентична на многих платформах, и функция не зависит от размер массива тоже, поэтому варианты 99 и 100 должны также создавать идентичный код, поэтому умный компилятор снова объединит экземпляры вместе.

В шаблонах нет никакого волшебства. Они не загадочным образом "раздувают" ваш код. Они просто предоставить вам инструмент, который позволяет легко создавать миллионы различных типов из одного и того же шаблона. Если вы действительно используете все эти типы, он должен генерировать код для всех из них. Как всегда с C ++, вы платите за то, что используете. Если вы используете как Array , Array , Array и Array , тогда вы получите четыре разных класса, потому что вы просили четыре разных класса. Если вы не запрашиваете четыре разных класса, они не будут Тебе что-нибудь.

7
ответ дан 1 December 2019 в 07:12
поделиться

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

Например:

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;
}

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

4
ответ дан 1 December 2019 в 07:12
поделиться

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

-1
ответ дан 1 December 2019 в 07:12
поделиться

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

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

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

Я запустил его, и получил 1, затем 2, что указывает на то, что он выполняется из одного и того же набора кода. Чтобы убедиться, что это действительно так, я создал другой массив с параметром size равным 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;
}
3
ответ дан 1 December 2019 в 07:12
поделиться

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

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

$ ~/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
ответ дан 1 December 2019 в 07:12
поделиться
Другие вопросы по тегам:

Похожие вопросы: