[БЕЗ_ЗВУКА] Прежде чем перейти к изучению неопределенного поведения, давайте познакомимся с некоторыми онлайн-сервисами для C++. Мы рассмотрим онлайн-сервисы, которые позволяют выполнять запуск программы; сервис, который позволяет измерять производительность отдельных частей программы; и сервис, который позволяет анализировать ассемблерный код, который компилятор генерирует для вашей программы. Зачем нам нужны эти онлайн-сервисы? Дело в том, что когда речь заходит о неопределенном поведении, то очень часто бывает необходимо посмотреть, как программа ведет себя с разными настройками компилятора, с разными компиляторами и с разными версиями одного и того же компилятора. Соответственно держать весь этот зоопарк на локальной машине бывает не очень удобно. В этом плане нам помогут онлайн-сервисы, где мы можем выбрать конкретный компилятор и его версию. Кроме того, онлайн-сервисы очень часто применяются на практике в повседневной разработке. Поэтому вам будет полезно с ними познакомиться. И кроме того, в онлайн-сервисах удобно делиться своей работой. Например, вы можете создать какую-то сессию, посмотреть, как выполняется определенная программа. И дальше послать эту ссылку вашему коллеге либо разместить ее в вопросе на Stack Overflow или на каком-нибудь другом ресурсе. Давайте начнем с сервиса под названием Wandbox, этот сервис позволяет писать и запускать программы на различных языках, кстати. Не только на C++, но для C++ он поддерживает компиляторы gcc и clang различных версий. Здесь можно выполнить некоторые настройки компилятора, указать, хотим ли мы смотреть на Warning'и, которые нам дает компилятор, хотим ли мы включить оптимизацию. Он также позволяет подключить несколько известных библиотек, таких как, например, Boost. Boost нам сейчас не нужен. Можно выбрать стандарт, давайте выберем последний стандарт, активный на данный момент. Это C++ 17. И кроме того, если нам этого недостаточно, мы можем в явном виде задать конкретные опции компилятора. Давайте напишем какую-нибудь простую программу. Выведем в выходной поток Hello, black belt! Вот такую программу мы написали, и давайте ее запустим. Кнопочка запуска. То есть наша программа компилируется, выполняется. В этом окошке мы можем видеть то, что она вывела в стандартный поток вывода. И после этого у нас пишется код возврата нашей программы. Ноль в данном случае — это значит, что все хорошо. После того как мы запустили программу, у нас есть кнопка Share, мы можем на нее нажать, и сервис сгенерирует нам уникальную ссылку, которую мы соответственно можем отправить коллеге или где-то ее опубликовать. Здесь все достаточно просто. Давайте теперь посмотрим на следующий сервис. quickbench.com — это сервис, который позволяет измерять быстродействие определенных фрагментов программы. У себя под капотом он использует фреймворк Google Benchmark — достаточно известный фреймворк, который позволяет запускать профилировку определенных фрагментов кода. Этот сервис позволяет зарегистрировать некоторые функции как бенчмарки, которые он будет проверять. Здесь мы видим две функции: StringCreation and StringCopy. И обе эти функции регистрируются во фреймворке с помощью макроса бенчмарк. Внутрь каждой функции передается объект под названием benchmark State, с помощью этого объекта можно выполнять некоторую конфигурацию фреймворка. Но самое главное, для чего нужен этот объект, по нему можно итерироваться. Дело в том, что в бенчмарках, как правило, проверяется некоторая операция, которая сама по себе занимает достаточно мало времени. И для того чтобы замерить реальное время ее выполнения, бывает полезно запустить ее множество раз подряд. То есть в цикле повторить по сути одну и ту же операцию. Сколько конкретно должно быть итераций, этих повторений, решать не очень удобно. И фреймфорк в принципе решает за вас. Он предоставляет вам этот объект, этот объект внутри себя реализует методы begin и end, поэтому его можно засунуть в ranged based for. Но поскольку здесь нам этот цикл нужен только для того, чтобы провести некоторое количество итераций, нам совсем не важно, какое именно значение у нас получается после доступа к этим методам begin и end, которые возвращают нам какой-то там утилитарный итератор. Для того чтобы подчеркнуть, что значение нас не интересует, мы используем переменную под названием подчеркивание. Это достаточно распространенная практика и не только в C++. Дальше у нас идет тело цикла — это то, что мы хотим измерить. В первой функции, как мы видим, мы создаем объект типа std : : string из строкового литерала. И после того как мы его создаем, мы делаем такой интересный вызов: benchmark : : DoNotOptimize. Дело в том, что само по себе создание объекта std : : string может не иметь некоторых наблюдаемых эффектов на программу, потому что эта переменная у нас нигде больше не используется. И поэтому, в принципе, компилятор может ее оптимизировать и этот цикл вырезать целиком. Поэтому нужно убедиться, что компилятор не попытается эту переменную вырезать. Для этого и используется такая конструкция benchmark : : DoNotOptimize. Внутри у нее реализация специфичная для каждого конкретного компилятора, и будем считать, что это просто некоторая магия, которая делается для нас фреймворком и которая позволяет убедиться, что компилятор не уберет лишнего. Соответственно вот сюда мы передаем наш created_string. И у нас есть вторая функция. Во второй функции мы делаем немного другое. Мы сначала создаем объект std : : string из этого строкового литерала, а потом в цикле мы выполняем копирование. Это стандартный пример, который предоставляет нам quickbench.com, для того чтобы познакомиться с профилированием. Соответственно здесь мы видим, что у нас в одном случае строчка создается из литерала, а во втором случае строчка создается из std : : string. И действительно это может занимать разное время. Давайте посмотрим. Дальше сервис предлагает нам выбрать компиляторов, которых мы хотим проверить, давайте выберем, допустим, gcc-8.2. Здесь представлены не самые последние версии компиляторов, к сожалению, в этом сервисе, но для большинства случаев нам этого должно быть достаточно. Выберем тоже C++ 17 и поставим оптимизацию O2, поскольку она используется чаще всего. И теперь запустим бенчмарк. Окей. Собственно теперь мы видим результаты. У нас есть два столбца, которые соответствуют нашим функциям, которые мы зарегистрировали с помощью макроса Benchmark. И размер каждого столбца показывает время, в течение которого выполнялась данная функция. Соответственно, мы можем видеть, что функция StringCreation выполнялась несколько дольше, чем функция StringCopy. Таким образом, мы можем сделать вывод, что создание строки из литерала занимает больше времени, чем создание строки с помощью копирования из другой строки. По крайней мере на данном компиляторе для данных настроек. Кроме того, следует обратить внимание на то, в чем измеряется вот это время. Оно измеряется в некоторых единицах. Вы можете видеть по оси ординат, в данном случае от 0 до 20. Одна единица соответствует некоторым накладным расходам, которые предоставляет сам бенчмарк. Потому что если мы напишем цикл, который не делает ничего, а просто крутится, это тоже займет некоторое время, правильно? И вот здесь мы можем посмотреть, сколько занимает такой цикл, который не делает ничего. Нажимаем вот эту галочку, и это называется Noop. Noop значит no operation, то есть не делаем ничего. Мы видим, что вот столько бы у нас работал наш бенчмарк, если бы он не делал ничего. А наши функции, которые все-таки что-то делали по сравнению с вот этим циклом, который не делает ничего, занимают столько-то времени. Соответственно, 15 и 18 единиц. После того как мы запустили бенчмарк, у нас адрес текущей страницы сгенерировался в некоторую уникальную ссылку. Вот здесь мы ее можем посмотреть, вот эту ссылку можем расшарить и кому-нибудь передать, чтобы они тоже посмотрели на результаты работы нашего бенчмарка. Окей. И теперь последний сервис Compiler Explorer. Этот сервис в отличие от предыдущих двух ничего не запускает. Этот сервис позволяет посмотреть ассемблерный код, который был сгенерирован компилятором для нашей программы. Даже не для программы на самом деле, здесь можно писать любой C++ код, потому что этот сервис ничего не будет запускать, ему нужно просто скомпилировать. Давайте для примера напишем вот такую простую функцию, которая принимает некоторое число и возвращает это же самое число. Вот такая простая функция, и мы видим, что здесь компилятор сгенерировал для нее некоторый ассемблерный код. Код состоит из одной инструкции. Не считая инструкцию ret, которая означает вернуться из текущей функции. Эта инструкция mov, eax и edi. mov — это то же самое, что оператор присваивания. Вот если навести мышкой, мы видим описание: копировать второй оператор в первый оператор. То здесь эффективно написано: eax присвоить edi. edi — это регистр, в котором помещаются входные параметры функции. eax — это регистр, в котором помещается входное значение функции. Соответственно здесь мы выходное значение присваиваем входному значению, что вполне логично и соответствует тому, что мы описали. Но у вас, наверное, в голове есть модель, что входные параметры и выходные значения помещаются на стек, и эта модель все еще верна, она работает. Но это модель. Компилятор, естественно, если он может обойтись без обращения к стеку, ведь стек лежит в памяти, обращение к памяти достаточно дорогое, он пробует все делать на регистрах. И поэтому на практике, после того как компилятор отработал и оптимизировал, он оставляет только обращение к регистрам. Окей. Сейчас мы с вами познакомились с сервисами, которые помогут нам разобраться в неопределенном поведении. А в следующих видео мы перейдем к его изучению.