Почему '\n' предпочтительнее, чем \n, для выходных потоков? - программирование

Почему '\n' предпочтительнее, чем \n, для выходных потоков?

В этом ответе мы можем прочитать следующее:

Я предполагаю, что есть небольшая разница между использованием '\n' или использованием "\n", но последний представляет собой массив (двух) символов, который должен быть напечатан символ за символом, для которого должен быть установлен цикл, что сложнее, чем вывод одного символа.

emphasis mine

Это имеет смысл для меня. Я думаю, что для вывода const char* требуется цикл, который будет проверять нулевой терминатор, который должен вводить больше операций, чем, скажем, простой putchar (не подразумевающий, что std::cout с char делегирует вызов - это просто упрощение, чтобы представить пример).

Это убедило меня использовать

std::cout << '\n';
std::cout << ' ';

а не

std::cout << "\n";
std::cout << " ";

Здесь стоит упомянуть, что я знаю, что разница в производительности в значительной степени незначительна. Тем не менее, некоторые могут утверждать, что предыдущий подход имеет намерение фактически передать один символ, а не строковый литерал, который оказался длиной char (два char, если считать '\0').

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

Затем я начал задаваться вопросом, насколько точно это изменение значимо, поэтому я побежал к Годболту. К моему удивлению, он показал следующие результаты при тестировании на GCC (транк) с флагами -std=c++17 -O3. Сгенерированная сборка для следующего кода:

#include <iostream>

void str() {
    std::cout << "\n";
}

void chr() {
    std::cout << '\n';
}

int main() {
    str();
    chr();
}

меня удивило, потому что кажется, что chr() фактически генерирует ровно вдвое больше инструкций, чем str():

.LC0:
        .string "\n"
str():
        mov     edx, 1
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        jmp     std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
chr():
        sub     rsp, 24
        mov     edx, 1
        mov     edi, OFFSET FLAT:_ZSt4cout
        lea     rsi, [rsp+15]
        mov     BYTE PTR [rsp+15], 10
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        add     rsp, 24
        ret

Почему это? Почему оба они в конечном итоге вызывают одну и ту же функцию std::basic_ostream с аргументом const char*? Означает ли это, что литеральный подход char не только не лучше, но на самом деле хуже строкового литерала?

4b9b3361

Ответ 1

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

Если вы посмотрите на сгенерированный код, вы увидите, что:

std::cout << '\n';

Компилируется до, по сути:

const char c = '\n';
std::cout.operator<< (&c, 1);

и чтобы это работало, компилятор должен сгенерировать кадр стека для функции chr(), откуда берутся многие дополнительные инструкции.

С другой стороны, при компиляции этого:

std::cout << "\n";

компилятор может оптимизировать str() для простого "хвостового вызова" operator<< (const char *), что означает, что стековый фрейм не требуется.

Таким образом, ваши результаты несколько искажены тем, что вы помещаете вызовы в operator<< в отдельные функции. Более показательно сделать эти вызовы встроенными, см.: https://godbolt.org/z/OO-8dS

Теперь вы можете видеть, что, хотя вывод '\n' все еще немного дороже (поскольку для ofstream::operator<< (char) нет специфической перегрузки), разница менее заметна, чем в вашем примере.

Ответ 2

Да, для этой конкретной реализации, для вашего примера, версия char немного медленнее, чем строковая версия.

Обе версии вызывают функцию стиля write(buffer, bufferSize). Для строковой версии bufferSize известен во время компиляции (1 байт), поэтому нет необходимости находить время выполнения нулевого терминатора. Для версии char компилятор создает небольшой 1-байтовый буфер в стеке, помещает в него символ и передает этот буфер для записи. Итак, версия char немного медленнее.

Ответ 3

Имейте в виду, что в сборке вы видите только создание стека вызовов, а не выполнение самой функции.

std::cout << '\n'; все еще намного немного быстрее, чем std::cout << "\n";

Я создал эту небольшую программу для измерения производительности, и она примерно в 20 раз немного быстрее на моей машине с g++ -O3. Попробуйте сами!

Изменение: Извините заметил опечатку в моей программе, и это не так много быстрее! Едва могу измерить разницу. Иногда один быстрее. В другой раз другой.

#include <chrono>
#include <iostream>

class timer {
    private:
        decltype(std::chrono::high_resolution_clock::now()) begin, end;

    public:
        void
        start() {
            begin = std::chrono::high_resolution_clock::now();
        }

        void
        stop() {
            end = std::chrono::high_resolution_clock::now();
        }

        template<typename T>
        auto
        duration() const {
            return std::chrono::duration_cast<T>(end - begin).count();
        }

        auto
        nanoseconds() const {
            return duration<std::chrono::nanoseconds>();
        }

        void
        printNS() const {
            std::cout << "Nanoseconds: " << nanoseconds() << std::endl;
        }
};

int
main(int argc, char** argv) {
    timer t1;
    t1.start();
    for (int i{0}; 10000 > i; ++i) {
        std::cout << '\n';
    }
    t1.stop();

    timer t2;
    t2.start();
    for (int i{0}; 10000 > i; ++i) {
        std::cout << "\n";
    }
    t2.stop();
    t1.printNS();
    t2.printNS();
}

Редактировать: как предложил geza, я попробовал 100000000 итераций для обоих, отправил его в /dev/null и запустил четыре раза. "\n" был один раз медленнее и в 3 раза быстрее, но не намного, но на других машинах он может быть другим:

Nanoseconds: 8668263707
Nanoseconds: 7236055911

Nanoseconds: 10704225268
Nanoseconds: 10735594417

Nanoseconds: 10670389416
Nanoseconds: 10658991348

Nanoseconds: 7199981327
Nanoseconds: 6753044774

Полагаю, мне было бы все равно.