Чем printf лучше cout

Чем printf лучше cout

Среда: C ++, VS 2010, Windows 7, 32-разрядная, Core-i7, 4 ГБ, 3,40 ГГц

Я проверил обе функции с count = 10000 за 5 раз каждый.
Измерили производительность с помощью QueryPerformanceCounter ,

850 миллисекунды (в среднем 5 пробежек)

9000 миллисекунды (в среднем 5 пробежек)

Означает ли это, что printf

10x быстрее чем кут

Редактировать:

С / Ox, / Ot, нет отладочной информации в Release build

и с std::ios_base::sync_with_stdio(false); в перфокауте,
результат одинаков для cout то есть

Изменить 2:

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

Решение

У меня больше не установлено VS 2010, но я провел быстрое тестирование с VS 2013 и 2015. Я немного изменил ваш код, чтобы уменьшить дублирование, и включил временный код, давая следующее:

С выключенной оптимизацией cout показал себя немного быстрее (например, 358 мс против 460 для printf ) но измерение скорости с отключенной оптимизацией довольно бессмысленно.

С включенной оптимизацией cout выиграл с еще большим отрывом (191 мс против 365 мс для printf ).

Чтобы сохранить их смысл, я запустил их все с перенаправленным выводом в файл. Без этого, по сути, все, что вы бы измерили, — это скорость драйвера консоли, которая по сути бессмысленна и бесполезна.

Собственно, вопрос в заголовке. Пожалуйста, дайте подробный ответ.

Ответы (1 шт):

Как завещал нам Страуструп, лучше потоки чем printf. Ибо потоки, как минимум, типобезопасны.

Например есть код:

Представим, что в процессе работы пришлось сменить тип переменной (это встречается сплошь и рядом):

Как видно, при использовании потока ничего не поменялось. А при использовании printf пришлось править форматную строку. Если printf много в коде, то везде править форматную строку во всех printf очень утомительно и черевато ошибками.

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

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

Кстати, перегрузить операции можно было бы и с printf. Или в потоках оставить форматную строку. Ну если все делать через одно место конечно.

Что касается квалификации персонала, то квалификация постоянно растет. Старая школа может долбать любимый printf до посинения, пока самим не надоест вылавливать ошибки форматов. Новое поколение не знает таких ужасов и весело применяет потоки.

Кстати, вывод через потоки в общем случае быстрее, чем вывод через printf. Объясняется это тем, что при выводе через потоки разбор типов происходит на этапе компиляции и в рантайме разбора типов нет, а там есть только вывод значения. В случае же с printf происходит разбор форматной строки в рантайме, то есть компилированный код передает управление интерпретатору форматной строки. А, как известно, интерпретатор работает в 100-1000 раз медленнее, чем компилированный код.

Ранние трансляторы С++ создавали потоковый вывод как надстройку над выводом printf и разницы в скорости не было (но все равно была разница в типобезопасности, так как форматные строки формировались автоматически). Современные трансляторы С++ делают потоковый вывод как надо, без использования printf. И это (теоретически) повышает быстродействие вывода.

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

Но и объем скомпилированного кода больше.

Объем скомпилированного кода при выводе в поток не больше, чем при использовании printf.

В этом примере строки вывода

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

скорость встроенных при компиляции (как следствие реализации templates) преобразований выше.

Скорость вывода при использовании printf меньше именно из-за того, что разбор форматной строки происходит в рантайме. В случае использования потоков нет разбора форматной строки в рантайме и также нет оверхеда из-за шаблонов потому что там вставляется не шаблон, а обычный вызов подпрограммы вывода для соответствующего типа. Это легко проверить, если посмотреть ассемблерный код простого примера, приведенного выше. Транслятор Visual Studio 2017.

Читайте также:  Клавиатура ноутбука печатает по две буквы

Видно, что вызов

разворачивается в обычный вызов подпрограммы

и никаких шаблонов тут нет.

мы пишем программы не для компьютера, а для других программистов

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

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

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

Что понадобилось бы для модернизации printf? Этот вопрос может показаться странным многим разработчикам, считающим, что C++ уже предоставляет современную замену printf. Хотя C++ Standard Library, несомненно, есть чем похвалиться, в том числе превосходной Standard Template Library (STL), она также включает библиотеку ввода-вывода на основе потоков данных (streams), которая совершенно непохожа на STL и не реализует ни одного из принципов STL, относящихся к эффективности.

Согласно определению Александра Степанова и Дэниэля Роуза (Daniel Rose) в их книге «From Mathematics to Generic Programming» (Addison-Wesley Professional, 2015), «обобщенное программирование — это подход к разработке, который фокусируется на проектировании алгоритмов и структур данных, способных работать в наиболее универсальных условиях без потери эффективности».

Если честно, ни printf, ни cout ни в коей мере не отражают современный C++. Функция printf является примером вариативной функции (т. е. функции с переменным количеством аргументов) (variadic function) и одним из немногих хороших применений этой весьма хрупкой функциональности, унаследованной от языка программирования C. Такие функции предшествовали появлению вариативных шаблонов (variadic templates). Последние предлагают по-настоящему современный и надежный механизм для поддержки варьируемого количества типов или аргументов. В противоположность этому в cout вообще не применяется такая вариативность, но интенсивно используются вызовы виртуальных функций, с которыми компилятор не может сделать ничего особенного для их оптимизации. Развитие архитектур процессоров пошло так, что printf явно получает преимущества и мало что делается для повышения производительности полиморфического подхода, заложенного в cout. Поэтому, если вам нужны производительность и эффективность, лучше выбрать printf. Кроме того, ее применение дает более четкий код. Вот пример:

Спецификатор преобразования %f сообщает printf, что здесь ожидается число с плавающей точкой, которое следует преобразовать в десятичную нотацию. Символ
— обычный символ новой строки, который можно дополнить символом перевода строки. Преобразование значений с плавающей точкой предполагает точность до шести знаков после десятичной точки. Таким образом, в этом примере будут выведены следующие знаки с последующей новой строкой:

Достижение того же результата с помощью cout поначалу кажется сравнительно простым:

Здесь cout полагается на оператор перегрузки, чтобы направить или послать число с плавающей точкой в поток вывода. Мне не нравится такое использование оператора перегрузки, но соглашусь, что это дело персонального стиля программирования. Наконец, endl завершает вставкой новой строки в поток вывода. Однако это не совсем то, что в примере с printf, и дает вывод с иной точностью после десятичной точки:

Тут возникает очевидный вопрос: как можно изменить точность соответствующих абстракций? Ну, если мне нужны лишь два знака после десятичной точки, можно просто указать это прямо в спецификаторе printf:

Теперь printf будет округлять число до двух знаков после десятичной точки:

Чтобы получить тот же результат в случае cout, потребуется набрать несколько больше кода:

Даже если вы не против многословия всего этого и вам нравится гибкость или выразительность такого варианта, учитывайте, что эта абстракция имеет свою цену. Прежде всего манипуляторы fixed и setprecision сохраняют состояние (stateful), а значит, их влияние остается до тех пор, пока вы не обратите их или не сбросите. В противоположность этому спецификатор преобразования в printf включает все, что нужно для одного конкретного преобразования, не влияя ни на какой другой код. Другие издержки могут не иметь никакого значения в большинстве случаев вывода, но в один прекрасный день вы вдруг заметите, что чужие программы способны выводить данные многократно быстрее, чем ваши. Помимо издержек от вызовов виртуальных функций, endl делает больше, чем вы, возможно, ожидали. Он не только отправляет знак новой строки в вывод, но и заставляет нижележащий поток сбрасывать свой вывод. При написании кода для любой разновидности ввода-вывода, будь то в консоль, файл на диске, сетевое соединение или даже в графическом конвейере, сброс обычно обходится очень дорого, а повторяющиеся сбросы, несомненно, приведут к падению производительности.

Читайте также:  Xiaomi mi band как подключить к телефону

Теперь, когда я немного исследовал и сравнил printf и cout, пора вернуться к изначальному вопросу: что понадобилось бы для модернизации printf? Разумеется, с появлением современного C++, примером чего является C++11 и более поздние стандарты, я могу повысить продуктивность труда и надежность printf, не жертвуя производительностью. Другой отчасти посторонний член C++ Standard Library — официальный класс string языка. Хотя репутация этого класса за прошедшие годы тоже была опорочена, он все же обеспечивает превосходную производительность. Хоть и не безгрешный, он предоставляет очень полезный способ обработки строк в C++. Поэтому любая модернизация printf должна на практике хорошо уживаться со string и wstring. Давайте посмотрим, что можно сделать. Сначала позвольте мне устранить то, что я считаю самой досадной проблемой printf:

На самом деле это должно было бы работать, но, уверен, вы отчетливо понимаете, что вместо этого результатом будет то, что ласково называют «неопределенным поведением». Как вам известно, вся суть printf в выводе текста, а C++-класс string является главным воплощением текста в языке C++. Мне нужно обернуть printf так, чтобы это просто работало. Я не хочу постоянно выдергивать завершаемый нулем символьный массив строки таким образом:

Это просто утомительно, поэтому я намерен исправить это обертыванием printf. Традиционно при этом писали другую вариативную функцию. Например, нечто в таком духе:

Увы, это ничего не дает мне. Возможно, было бы удобно обернуть некую разновидность printf, чтобы записывать в какой-то другой буфер, но в данном случае я не получил бы ничего полезного. Я не хочу возвращаться к вариативным функциям в стиле C. Вместо этого я стремлюсь к использованию возможностей современного C++. К счастью, благодаря вариативным шаблонам в C++11 мне никогда больше не придется писать вариативную функцию. Вместо обертывания printf в другую вариативную функцию ее можно обернуть в вариативный шаблон:

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

это привело бы к раскрытию пакета аргументов args, состоящего из 123 и 456, внутри тела вариативного шаблона, словно я написал просто:

Так что же я выиграл? Конечно, я вызываю printf, а не vprintf, и мне не нужно управлять va_list и связанными макросами, крутящими стек, но, тем не менее, я просто пересылаю аргументы. Однако пусть вас не обманывает простота этого решения. Компилятор вновь будет распаковывать аргументы шаблона функции так, будто я напрямую вызвал printf, а значит, в обертывании printf таким способом не будет никаких издержек. Это также означает, что она по-прежнему остается полноправным элементом C++ и что я могу задействовать мощные методы метапрограммирования в этом языке для встраивания любого необходимого кода, причем с полным обобщением. Вместо простого раскрытия пакета параметров args можно обернуть каждый аргумент, чтобы добавить к нему любую настроечную информацию, необходимую printf. Рассмотрим следующий шаблон функции:

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

Функцию Print можно вызывать прежним образом:

Но теперь это приводит к следующему раскрытию:

Это очень интересно. Конечно, для этих целочисленных аргументов никакой разницы нет, но теперь можно перегружать функцию Argument для обработки строковых классов C++:

Далее я просто вызываю функцию Print с какими-нибудь строками:

Компилятор в конечном счете раскроет внутреннюю функцию printf следующим образом:

Это гарантирует, что завершаемый нулем массив символов каждой строки передается в printf и обеспечивает четко определенное поведение:

Наряду с шаблоном функции Print я также использую ряд перегруженных версий для неформатированного вывода. Это обычно безопаснее и избавляет printf от случайной интерпретации произвольных строк как содержащих спецификаторы преобразования. Эти функции перечислены на рис. 1.

Рис. 1. Отображение неформатированного вывода

Первые две перегруженные версии просто форматируют обычный и «широкосимвольный» массивы соответственно. Заключительный шаблон функции перенаправляет в соответствующую перегруженную версию в зависимости от типа аргумента: string или wstring. Благодаря этим функциям можно безопасно выводить некоторые спецификаторы преобразования в буквальном виде:

Это снимает самую распространенную проблему с printf, безопасно и прозрачно обрабатывая строковый вывод. А как насчет форматирования самих строк? C++ Standard Library предоставляет различные вариации printf для записи в буферы строк символов. Из них я нахожу наиболее эффективными snprintf и swprintf. Эти две функции обрабатывают символьный и широкосимвольный вывод соответственно. Они позволяют указывать максимальное количество символов, которое может быть записано, и возвращают значение, с помощью которого можно вычислить, какое пространство понадобится, если исходный буфер окажется недостаточно большим. Тем не менее, сами по себе они подвержены ошибкам и весьма утомительны в использовании. Пора внести в них кое-что из современного C++.

Читайте также:  Картридж для sony dpp fp90

C не поддерживает перегрузку функций, но в C++ использовать ее весьма удобно, и это открывает дверь в обобщенное программирование, поэтому я начну с обертывания snprintf и swprintf как функций, вызываемых StringPrint. Кроме того, я задействую шаблоны вариативных функций, чтобы использовать преимущества безопасного раскрытия аргументов, ранее примененного для функции Print. На рис. 2 приведен код для обеих функций. Эти функции также проверяют, что результат не равен –1, а именно такое значение возвращают нижележащие функции, когда возникает какая-либо поправимая проблема при разборе форматирующей строки. Я использую контрольное выражение (assertion), так как просто предполагаю, что это ошибка и что ее надо исправить до распространения производственного кода. Возможно, вы захотите заменить его на исключение, но учитывайте, что надежного способа преобразования всех ошибок в исключения нет, поскольку все равно можно передать недопустимые аргументы, которые приведут к неопределенному поведению. Современный C++ не является защищенным от дураков C++.

Рис. 2. Низкоуровневые функции форматирования строки

Функции StringPrint обеспечивают обобщенный способ операций с форматированием строк. Теперь можно сосредоточиться на специфике класса string, и работа с ним по большей части требует управления памятью. Я хотел бы писать код примерно так:

Здесь нет видимого управления буферами. Мне не нужно выяснять, насколько большой буфер требуется создать. Я лишь прошу функцию Format логически присвоить отформатированный вывод объекту string. Как обычно, Format может быть шаблоном функции, а именно вариативным:

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

Приращение на 1 необходимо, так как и snprintf, и swprintf предполагают, что сообщаемый размер буфера включает место для завершающего нулевого символа. Это работает достаточно хорошо, но должно быть очевидно, что с производительностью здесь все очень плохо. Подход, обеспечивающий гораздо более высокую производительность в большинстве случаев, — предполагать, что строка достаточно велика, чтобы хранить форматированный вывод и выполнять подгонку размера буфера только при необходимости. Это почти выворачивает предыдущий код наизнанку, но в этом случае код весьма надежен. Я начинаю с попытки отформатировать строку непосредственно в буфере:

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

Если полученный размер меньше размера строки, значит, форматирование успешно выполнено, но буфер нужно отсечь под этот размер:

Наконец, если размеры совпадают, ничего делать не надо и функция Format может просто вернуть управление. Полный шаблон функции Format представлен на рис. 3. Если вы знакомы с классом string, то, возможно, вспомните, что он также сообщает свою вместимость (capacity), и у вас может возникнуть соблазн присвоить размеру строки его вместимость до первого вызова StringPrint, полагая, что это улучшит ваши шансы на корректное форматирование строки с первого захода. Вопрос в том, а можно ли изменить размер объекта string быстрее, чем printf сумеет разобрать его форматирующую строку и вычислить необходимый размер буфера. Основываясь на своих неформальных тестах, могу дать ответ: когда как. Видите ли, изменение размера string для соответствия его вместимости требует несколько большего простого изменения сообщаемого размера. Нужно очистить любые дополнительные символы, а это требует времени. Окажется ли это быстрее, чем printf разберет форматирующую строку, зависит от того, сколько символов придется очистить и насколько сложным должно быть форматирование. Я применяю даже более скоростной алгоритм для вывода больших объемов строковых данных, но нахожу, что функция Format на рис. 3 обеспечивает хорошую производительность в большинстве сценариев.

Рис. 3. Форматирование строк

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

Или отформатировать числа с плавающей точкой:

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

Это просто набор полезных функций из моей библиотеки вывода для современного C++. Надеюсь, они в какой-то мере подскажут вам, как использовать современный C++ для обновления старых методов программирования на C и C++. Кстати, в моей библиотеке вывода определены функции Argument, а также низкоуровневые функции StringPrint во вложенном пространстве имен Internal. Это помогает держать библиотеку легко читаемой и простой для понимания, но вы можете упорядочить свою реализацию как пожелаете.

Ссылка на основную публикацию
Adblock detector