Несколько вызовов printf() против одного вызова printf() с длинной строкой?
Скажем, у меня есть одна строка printf()
с длинной строкой:
printf( "line 1\n"
"line 2\n"
"line 3\n"
"line 4\n"
"line 5\n"
"line 6\n"
"line 7\n"
"line 8\n"
"line 9\n.. etc");
Каковы затраты, понесенные этим стилем по сравнению с наличием нескольких printf()
для каждой строки?
Будет ли возможное переполнение стека, если строка слишком длинная?
Ответы
Ответ 1
Каковы затраты, понесенные этим стилем, по сравнению с наличием нескольких printf() для каждой строки?
Несколько printf
приведет к нескольким вызовам функций и только к служебным.
Будет ли возможное переполнение стека, если строка слишком длинная?
В этом случае нет. Строковые литералы обычно хранятся в памяти только для чтения, а не в стеке. Когда строка передается в printf
, то только стек указателя на его первый элемент копируется в стек.
Компилятор будет обрабатывать эту многострочную строку "строка 1\n"
"line 2\n"
"line 3\n"
"line 4\n"
"line 5\n"
"line 6\n"
"line 7\n"
"line 8\n"
"line 9\n.. etc"
как одиночная строка
"line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\n.. etc"
и это будет сохранено в разделе только для чтения памяти.
Но учтите, что (указано pmg в comment) в стандартном разделе C11 5.2.4.1 Лимиты перевода говорят, что
Реализация должна иметь возможность переводить и выполнять хотя бы одну программу, содержащую хотя бы один экземпляр каждого из следующих пределов 18):
[...]
- 4095 символов в строковом литерале (после конкатенации)
[...]
Ответ 2
C объединяет строковые литералы, если они не разделены ничем или пробелом. Итак, ниже
printf( "line 1\n"
"line 2\n"
"line 3\n"
"line 4\n"
"line 5\n"
"line 6\n"
"line 7\n"
"line 8\n"
"line 9\n.. etc");
отлично подходит и выделяется с точки зрения удобочитаемости. Кроме того, один вызов printf
бесцельно имеет меньшие накладные расходы, чем 9 printf
вызовов.
Ответ 3
printf
является медленной функцией, если вы выводите только постоянные строки, потому что printf
должен сканировать каждый символ для спецификатора формата (%
). Такие функции, как puts
, значительно быстрее для длинных строк, потому что они могут в принципе просто memcpy
вводить строку в выходной буфер ввода/вывода.
Многие современные компиляторы (GCC, Clang, возможно, другие) имеют оптимизацию, которая автоматически преобразует printf
в puts
, если входная строка является константной строкой без спецификаторов формата, которая заканчивается новой строкой. Так, например, компиляция следующего кода:
printf("line 1\n");
printf("line 2\n");
printf("line 3"); /* no newline */
приводит к следующей сборке (Clang 703.0.31, cc test.c -O2 -S
):
...
leaq L_str(%rip), %rdi
callq _puts
leaq L_str.3(%rip), %rdi
callq _puts
leaq L_.str.2(%rip), %rdi
xorl %eax, %eax
callq _printf
...
Другими словами, puts("line 1"); puts("line 2"); printf("line 3");
.
Если ваша длинная строка printf
не заканчивается новой строкой, то ваша производительность может быть значительно хуже, чем если бы вы сделали кучу вызовов printf
с строками с завершающим расширением строки, просто из-за этой оптимизации. Чтобы продемонстрировать, рассмотрите следующую программу:
#include <stdio.h>
#define S "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
#define L S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S S
/* L is a constant string of 4000 'a */
int main() {
int i;
for(i=0; i<1000000; i++) {
#ifdef SPLIT
printf(L "\n");
printf(S);
#else
printf(L "\n" S);
#endif
}
}
Если SPLIT
не определен (создается одиночный printf
без завершающей новой строки), время выглядит следующим образом:
[08/11 11:47:23] /tmp$ cc test.c -O2 -o test
[08/11 11:47:28] /tmp$ time ./test > /dev/null
real 0m2.203s
user 0m2.151s
sys 0m0.033s
Если SPLIT
определен (создается два printf
s, один с завершающей новой строкой, другой без), время выглядит следующим образом:
[08/11 11:48:05] /tmp$ time ./test > /dev/null
real 0m0.470s
user 0m0.435s
sys 0m0.026s
Итак, вы можете видеть, что в этом случае разделение printf
на две части на самом деле приводит к ускорению 4 раза. Конечно, это крайний случай, но он иллюстрирует, как printf
можно оптимизировать в зависимости от ввода. (Обратите внимание, что использование fwrite
еще быстрее - 0,197 с, поэтому вам следует подумать об использовании этого, если вы действительно хотите скорость!).
tl; dr: если вы печатаете только большие константные строки, полностью избегайте printf
и используйте более быструю функцию, например puts
или fwrite
.
Ответ 4
A printf
без модификаторов формата молча заменяется (ака оптимизирован) на вызов puts
. Это уже ускорение. Вы действительно не хотите потерять это при вызове printf
/puts
несколько раз.
GCC имеет printf
(среди прочих) как встроенный, поэтому он может оптимизировать вызовы во время компиляции.
См:
Ответ 5
Каждый дополнительный printf (или помещает, если ваш компилятор оптимизирует его таким образом) каждый раз будет нести системную служебную нагрузку на функцию, хотя есть хорошая вероятность, что оптимизация все равно их объединит.
Мне еще предстоит увидеть реализацию printf, которая была листовой функцией, поэтому ожидайте дополнительные накладные расходы на вызовы для чего-то вроде vfprintf, и она вызывает запрос.
Тогда у вас, вероятно, будут какие-то накладные расходы на системные вызовы для каждой записи. Поскольку printf использует stdout, который буферизуется, некоторые из этих (действительно дорогостоящих) переключателей контекста обычно можно избежать... кроме всех вышеприведенных примеров с новыми строками. Большая часть ваших расходов, вероятно, будет здесь.
Если вы действительно беспокоитесь о стоимости в своем основном потоке, переместите этот материал в отдельный поток.