Может ли использование C ++ 11 'auto' улучшить производительность?

230

Я понимаю, почему autoтип в C ++ 11 улучшает правильность и удобство обслуживания. Я читал, что это также может улучшить производительность ( почти всегда авто от Херба Саттера), но я упускаю хорошее объяснение.

  • Как можно autoулучшить производительность?
  • Кто-нибудь может привести пример?
DaBrain
источник
5
См. Grassutter.com/2013/06/13/…, где говорится об избежании случайных неявных преобразований, например, из гаджета в виджет. Это не обычная проблема.
Джонатан Уэйкли,
42
Принимаете ли вы «снижает вероятность непреднамеренной пессимизации» как улучшение производительности?
5gon12eder
1
Выполнение очистки кода только в будущем, возможно
Croll
Нам нужен короткий ответ: нет, если ты хороший. Это может предотвратить "нубийские" ошибки. C ++ имеет кривую обучения, которая убивает тех, кто не делает это в конце концов.
Алек Тил

Ответы:

309

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

std::map<Key, Val> m;
// ...

for (std::pair<Key, Val> const& item : m) {
    // do stuff
}

Видишь ошибку? Здесь мы думаем, что элегантно берем каждый элемент на карте с помощью константной ссылки и используем новое выражение диапазона для того, чтобы прояснить наше намерение, но на самом деле мы копируем каждый элемент. Это происходит потому , что std::map<Key, Val>::value_typeэто std::pair<const Key, Val>не std::pair<Key, Val>. Таким образом, когда мы (неявно) имеем:

std::pair<Key, Val> const& item = *iter;

Вместо того, чтобы брать ссылку на существующий объект и оставлять его при этом, мы должны выполнить преобразование типа. Вам разрешено принимать константную ссылку на объект (или временный) другого типа при условии, что доступно неявное преобразование, например:

int const& i = 2.0; // perfectly OK

Преобразование типов является допустимым неявным преобразованием по той же причине, по которой вы можете преобразовать a const Keyв a Key, но мы должны создать временный объект нового типа, чтобы учесть это. Таким образом, наш цикл эффективно:

std::pair<Key, Val> __tmp = *iter;       // construct a temporary of the correct type
std::pair<Key, Val> const& item = __tmp; // then, take a reference to it

(Конечно, на самом деле нет __tmpобъекта, он есть просто для иллюстрации, на самом деле неназванный временный объект просто обязан itemдля своей жизни).

Просто меняется на:

for (auto const& item : m) {
    // do stuff
}

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

Барри
источник
19
@ Барри Можете ли вы объяснить, почему компилятор с радостью делал копии, а не жаловался на то, что пытался воспринимать это std::pair<const Key, Val> const &как std::pair<Key, Val> const &? Новичок в C ++ 11, не уверен, как диапазон и autoиграет в этом.
Агоп
@ Барри Спасибо за объяснение. Это часть, которую я пропустил - по какой-то причине я подумал, что вы не можете иметь постоянную ссылку на временную. Но, конечно, вы можете - он просто перестанет существовать в конце своей области.
Агоп
@ Барри, я вас понимаю, но проблема в том, что тогда нет ответа, который охватывал бы все причины, чтобы использовать autoэто повышение производительности. Поэтому я напишу это своими словами ниже.
Якк - Адам Невраумонт
38
Я до сих пор не думаю, что это доказательство того, что « autoулучшает производительность». Это просто пример, который « autoпомогает предотвратить ошибки программистов, которые снижают производительность». Я утверждаю, что между ними есть тонкое, но важное различие. Тем не менее +1.
Гонки легкости на орбите
70

Поскольку autoвыводит тип инициализирующего выражения, преобразование типов не выполняется. В сочетании с шаблонными алгоритмами это означает, что вы можете получить более прямые вычисления, чем если бы вы сами составляли тип - особенно когда вы имеете дело с выражениями, тип которых вы не можете назвать!

Типичный пример взят из (ab) с использованием std::function:

std::function<bool(T, T)> cmp1 = std::bind(f, _2, 10, _1);  // bad
auto cmp2 = std::bind(f, _2, 10, _1);                       // good
auto cmp3 = [](T a, T b){ return f(b, 10, a); };            // also good

std::stable_partition(begin(x), end(x), cmp?);

С помощью cmp2и cmp3, весь алгоритм может встроить вызов сравнения, тогда как если вы создаете std::functionобъект, вызов не только не может быть встроенным, но вы также должны пройти полиморфный поиск в стертой внутренней стороне оболочки функции.

Еще один вариант на эту тему, что вы можете сказать:

auto && f = MakeAThing();

Это всегда ссылка, привязанная к значению выражения вызова функции, и никогда не создающая никаких дополнительных объектов. Если вы не знаете тип возвращаемого значения, вы можете быть вынуждены создать новый объект (возможно, как временный) с помощью чего-то подобного T && f = MakeAThing(). (Более того, auto &&даже работает, когда возвращаемый тип не является подвижным, а возвращаемое значение является prvalue)

Керрек С.Б.
источник
Так что это причина «избежать стирания типа» auto. Ваш другой вариант - «избегать случайных копий», но требует приукрашивания; почему autoвы просто набираете шрифт? (Я думаю, что ответ «вы ошиблись в типе, и он молча преобразуется»). Что делает этот пример менее понятным примером ответа Барри, нет? То есть, есть два основных случая: автоматический, чтобы избежать стирания типа, и автоматический, чтобы избежать ошибок тихого типа, которые случайно конвертируются, оба из которых имеют стоимость времени выполнения.
Якк - Адам Невраумонт
2
«не только звонок не может быть встроен» - почему это тогда? Вы имеете в виду, что в принципе что-то предотвращает девиртуализацию вызова после анализа потока данных, если соответствующие специализации std::bind, std::functionи std::stable_partitionвсе были встроены? Или просто, что на практике ни один компилятор C ++ не будет достаточно агрессивно встроен, чтобы разобраться в беспорядке?
Стив Джессоп
@SteveJessop: В основном последний - после того, как вы пройдете через std::functionконструктор, будет очень сложно увидеть фактический вызов, особенно с оптимизацией небольших функций (так что вы на самом деле не хотите девиртуализации). Конечно в принципе все как-будто ...
Керрек С.Б.
41

Есть две категории.

autoможно избежать стирания типа. Существуют неустранимые типы (например, лямбда-выражения) и почти неузнаваемые типы (например, результат std::bindили другие выражения-шаблоны, подобные вещам)

Без autoэтого вам в конечном итоге придется стирать данные до чего-то вроде std::function. Тип стирания имеет затраты.

std::function<void()> task1 = []{std::cout << "hello";};
auto task2 = []{std::cout << " world\n";};

task1имеет издержки на стирание типа - возможное выделение кучи, сложность его выделения и накладные расходы на вызов таблицы виртуальных функций. task2не имеет ни одного Лямбды нужно авто или другие формы удержания типа, чтобы хранить без стирания типа; другие типы могут быть настолько сложными, что им это нужно только на практике.

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

Foo const& f = expression();

скомпилирует, если expression()возвращает Bar const&или Barили даже Bar&, откуда Fooможно построить Bar. FooБудет создан временный объект, а затем привязан к нему f, и его срок жизни будет продлен до тех пор, пока он fне исчезнет.

Программист, возможно, имел в виду Bar const& fи не намеревался сделать копию там, но копия сделана независимо.

Наиболее распространенным примером является тип *std::map<A,B>::const_iterator, который std::pair<A const, B> const&не является std::pair<A,B> const&, но ошибка - это категория ошибок, которые бесшумно снижают производительность. Вы можете построить std::pair<A, B>из std::pair<const A, B>. (Ключ на карте - const, потому что редактирование - плохая идея)

Оба @Barry и @KerrekSB сначала проиллюстрировали эти два принципа в своих ответах. Это просто попытка выделить два вопроса в одном ответе с формулировкой, которая направлена ​​на проблему, а не на пример.

Якк - Адам Невраумонт
источник
9

Существующие три ответа дают примеры , когда с помощью autoпомогает «делает его менее вероятно, непреднамеренно pessimize» эффективно делает его «повысить производительность».

Есть обратная сторона монеты. Использование autoс объектами, у которых есть операторы, которые не возвращают базовый объект, может привести к некорректному (все еще компилируемому и запускаемому) коду. Например, этот вопрос спрашивает, как использование autoдало разные (неправильные) результаты, используя библиотеку Eigen, то есть следующие строки

const auto    resAuto    = Ha + Vector3(0.,0.,j * 2.567);
const Vector3 resVector3 = Ha + Vector3(0.,0.,j * 2.567);

std::cout << "resAuto = " << resAuto <<std::endl;
std::cout << "resVector3 = " << resVector3 <<std::endl;

привело к другому выводу. Следует признать, что это в основном связано с ленивой оценкой Eigens, но этот код / ​​должен быть прозрачным для пользователя (библиотеки).

Хотя производительность здесь не сильно пострадала, использование autoво избежание непреднамеренной пессимизации может быть классифицировано как преждевременная оптимизация или, по крайней мере, неправильно;).

Ави Гинзбург
источник
1
Добавил противоположный вопрос: stackoverflow.com/questions/38415831/…
Леон