Почему ++ может отличаться от i + = 1 по производительности

По-видимому, после прочтения старого названия, которое было

Почему такие вопросы, как is ++i fster than i+=1 даже существуют?

люди не удосужились полностью прочитать вопрос.

Вопрос не в том, почему люди спрашивают об этом! Речь шла о том, почему компилятор когда-либо делает разницу между ++i и i+=1 , и есть ли возможные сценарии, где это имеет смысл. Хотя я ценю все ваши остроумные и глубокие комментарии, мой вопрос был не в этом.


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

Использование ++ i над i = i + 1 дает вам преимущество в производительности.

Я не увлечен этим конкретным примером, скорее говоря более или менее вообще.

Очевидно, когда автор писал книгу, это имело для него смысл, он не просто делал это. Мы знаем, что современные компиляторы не заботятся о том, используете ли вы ++i , i+=1 или i = i + 1 , код будет оптимизирован, и у нас будет такой же выход asm.

Это кажется вполне логичным: если две операции выполняют одно и то же и имеют одинаковый результат, нет никаких оснований компилировать ++i в одну вещь, а i+=1 в другую.

Но так как автор книги написал это , он видел разницу! Это означает, что некоторый компилятор фактически произведет различный вывод для этих двух строк. Это означает, что у парней, у которых был компилятор, были некоторые причины рассматривать ++i и i+=1 разному. Мой вопрос в том, почему они это сделают?

Это просто потому, что было сложно / невозможно сделать компиляторы достаточно продвинутыми для выполнения таких оптимизаций в те дни? Или, может быть, на некоторых очень специфических платформах / оборудовании / в каком-то специальном сценарии действительно имеет смысл сделать разницу между ++i и i+=1 и другими вещами такого рода? Или, может быть, это зависит от типа переменной? Или разработчики компилятора просто ленивы?

Представьте себе не оптимизирующий компилятор. На самом деле все равно, является ли ++i эквивалентом i+=1 или нет, он просто испускает первое, что может думать об этом. Он знает, что у процессора есть инструкция для добавления, и он знает, что у процессора есть инструкция для увеличения целого числа. Итак, предполагая, что у i есть тип int , тогда для ++i он испускает что-то вроде:

 inc  

При i+=1 он испускает что-то вроде:

 load the constant 1 into a register add  to that register store that register to  

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

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

поскольку автор книги написал это, он видел разницу

Не обязательно. Если вы видите высказывание о том, что «быстрее», иногда автор книги глупее, чем вы и компилятор. Иногда он умный, но он умело формировал свои эмпирические правила в условиях, которые больше не применяются. Иногда он размышлял о существовании компилятора как немого, как тот, который я описал выше, не проверяя, действительно ли какой-либо компилятор, который вы когда-либо использовали, действительно был тупым. Как я только что сделал 😉

Btw, 10 лет назад, слишком поздно для достойного компилятора с включенной оптимизацией, чтобы не сделать эту конкретную оптимизацию. Точная временная шкала, вероятно, не имеет отношения к вашему вопросу, но если автор написал это, и их оправдание было «это было еще в 2002 году», то лично я бы этого не принял. Заявление было не более правильным, чем сейчас. Если они сказали 1992 год, то ОК, лично я не знаю, какие компиляторы были похожи на то, что я не мог им противоречить. Если бы они сказали 1982, то я все равно был бы подозрительным (в конце концов, C ++ был придуман тогда. Большая часть его дизайна опирается на оптимизирующий компилятор, чтобы избежать огромной много расточительной работы во время выполнения, но я дам, что самым большим пользователем этого факта являются шаблонные контейнеры / алгоритмы, которых не было в 1982 году). Если бы они сказали 1972, я бы, вероятно, просто поверил им. Конечно, был период, когда компиляторы С были прославленными ассемблерами.

В C, i++ обычно не эквивалентен i=i+1 потому что они производят разные значения выражения. ++i эквивалентно i=i+1 потому что они дают одно и то же значение выражения.

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

Эта временная переменная оживает, потому что i++ диктует следующие две вещи:

  1. исходное значение i возвращается выражением i++
  2. i увеличивается на 1

Если вы сначала берете исходное значение i а затем увеличиваете i , ну, исходное (теперь старое) значение i должно где-то жить (память или регистр, не имеет значения), потому что он не может жить в уже увеличившейся переменной i . Это ваша временная переменная.

Если, OTOH, вы сначала увеличиваете i на 1, то снова вам нужно создать где-нибудь (в регистре или памяти) значение, равное i-1 чтобы отменить приращение, поэтому можно получить старое (pred-incremented) значение как результат выражения i++ .

С ++i и i=i+1 вещи намного проще. Эти выражения определяют 2 вещи:

  1. i получаю прирост
  2. возвращается новое значение i

Здесь естественно просто сначала увеличить i а затем принять его значение. Вам не нужно иметь пару значений i и i+1 (или i-1 и i ), старых и новых. Новым является все, что нам нужно здесь.

Теперь, с тех пор, когда компиляторы не очень хорошо оптимизировались, есть старые книги и старые люди. Оттуда можно понять, что i++ может быть медленнее, чем ++i . Разница наблюдалась на практике, а не составлялась. Это было реально, и некоторые могут подумать, что это может быть так и сегодня.

Можно также попытаться проанализировать разницу между двумя (тремя) приращающимися выражениями и увидеть, что действительно может потребоваться сделать некоторые дополнительные операции и использовать дополнительную ячейку памяти для временной переменной в случае i++ . И в этот момент человек может не увидеть, когда это временное не является необходимым или как определить, необходимо ли это или нет. Это еще одна возможность для вопросов об упомянутой разнице.

И, конечно, люди любили троллинг во все времена. 🙂

Что касается разработчиков компилятора ленивых … Я не думаю, что они были. Вот почему.

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

Написание достойного оптимизационного компилятора возможно даже тогда.

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

Пример: я. У меня был доступ к Turbo C / C ++ от Borland в середине 90-х. Но я не рассматривал возможность обучения и использования C до конца 90-х, в начале 0-х. Причина? Borland C / C ++ был намного медленнее, чем их Pascal, и мой компьютер был не очень хорошим. Ожидание компиляции кода было болезненным. И так оно и было. Я сначала освоил Паскаль и только позже вернулся к C и C ++.

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

Вы также не должны забывать, что разработка и управление большой частью кода компилятора с рудиментарными инструментами тех дней тоже не была очень интересной. Только теперь у вас может быть хорошая IDE (и не одна!) С отладчиком в ней, подсветка синтаксиса, автоматическое завершение и все, управление исходным кодом, простое сравнение файлов, интернет / StackOverflow и т. Д. И т. Д. … И мы можем теперь есть несколько 20+ дисплеев, подключенных к ПК! Теперь мы говорим о производительности! 🙂

Действительно, сегодня у нас есть прекрасные инструменты и устройства. 20, 30, 40 лет назад люди могли только представить или предсказать их, но пока не использовать.

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

Поэтому я сомневаюсь, что компиляторы были ленивы.

Посмотрите в Интернете на то, что называется Small C Это общий термин для многоstreamового и функционально уменьшенного компилятора C, реализующего только самые важные функции языка C. Вы найдете некоторые реализации Ron Cain , James Hendrix (ранние 80-е годы) и других и производных от них (например, реализация RatC / Lancaster этого же Bob Berry и Brian Meekings ).

Если вы посмотрите на код любого из этих Small C's , вы обнаружите, что минимальный размер кода составляет около 50+ КБ с 2+ KLOC, и это всего лишь переводчик с C на ассемблерный код! Кому-то в какой-то момент нужно будет собрать это с помощью ассемблера.

Я не могу себе представить, как удобно работать с таким проектом на чем-то вроде 8-битного домашнего компьютера, например ZX-Spectrum (который у меня был в детстве), который мог иметь максимум 48 КБ ОЗУ, процессор работал на ~ 3 МГц , все хранилище находилось на магнитофоне, а скорость передачи данных составляла 10 КБ / мин, а экран был 32х24, даже не 80х25.

И весь этот небольшой код C с начала 80-х годов, едва вписывающийся в память компьютера, ничего не оптимизировал!

Я не совсем понимаю, к какой книге вы обращаетесь, и, таким образом, не можете найти исходную цитату. Тем не менее, я подозреваю, что автор действительно не говорил о встроенных типах. Для встроенных типов выражения ++i , i += 1 и i = i + 1 эквивалентны, и компилятор, скорее всего, выберет наиболее эффективный, но для других типов, например, любой iterator с произвольным доступом, они не обязательно эквивалентны. Семантически, они должны быть эквивалентными, но компилятор не имеет этого семантического знания, и реализация может делать разные вещи в любом случае. Привыкание к написанию формы, которая может быть наиболее эффективной при использовании объектов типов classов, даже при использовании встроенных типов, позволяет избежать ненужных проблем с производительностью: вы используете наиболее эффективный способ «автоматически», поэтому не нужно платить слишком много внимания.

При определении classа, который предоставляет соответствующие операторы, например, при создании iteratorа с произвольным доступом, компилятор, возможно, не сможет определить, что код эквивалентен. Одной из причин этого является то, что код не обязательно отображается, например, когда функции не встроены. Даже когда функции встроены, могут возникнуть побочные эффекты, которые компилятор не может отслеживать. Реализация iteratorов с произвольным доступом может очень хорошо внутренне использовать указатели и использовать ++p и p += n . Однако в тот момент, когда информация о том, что n является константой значения 1 , теряется, она больше не может заменить p += n на ++p . Хотя компиляторы хороши при постоянной сгибании, это, как минимум, требует, чтобы весь код был встроенным и что компилятор решил, что встроенная функция действительно должна быть встроена.

Ответ зависит от типа i .

Когда class реализован, существуют разные операторы для предварительного инкремента ( T & T::operator++() , post-increment ( TT::operator ++(int)) , добавление ( TT::operator +(T const &) (и т. д.)) и приращение ( TT::operator +=(T const &) ). (Есть все варианты из них, очевидно)

Для достаточно тривиальных типов это, вероятно, все мухны.

Однако для нетривиальных типов производительность будет зависеть от того, как они написаны. В общем:

  • a++ вряд ли будет быстрее, чем ++a , потому что ему нужно вернуть копию объекта до приращения.
  • a = a + b вряд ли будет быстрее, чем a += b поскольку первое требует создания временного.
  • a += 1 вряд ли будет быстрее, чем ++a потому что 1 может не быть тем же типом, что и a, и могут быть некоторые связанные с этим расходы и делать все, что необходимо для его устранения.
  • Для некоторых classов некоторые из этих операций могут быть недоступны в любом случае.

Помимо этого, вы можете с уверенностью сказать, что вам следует просмотреть код и выполнить тесты производительности.