Подтвердить что ты не робот

Несколько вызовов 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() для каждой строки?
Будет ли возможное переполнение стека, если строка слишком длинная?

4b9b3361

Ответ 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, который буферизуется, некоторые из этих (действительно дорогостоящих) переключателей контекста обычно можно избежать... кроме всех вышеприведенных примеров с новыми строками. Большая часть ваших расходов, вероятно, будет здесь.

Если вы действительно беспокоитесь о стоимости в своем основном потоке, переместите этот материал в отдельный поток.