Почему этот метод с использованием putchar_unlocked медленнее, чем printf и cout для печати строк?

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

В настоящее время я использую поточно-небезопасную функцию putchar_unlocked для печати некоторых тестов. Я полагал, что эта функция была быстрее, чем cout e printf для некоторых типов данных, если была хорошо реализована из-за своей природы, допускающей разблокировку потоков.

Я реализовал функцию для печати строк таким образом (очень просто, с моей точки зрения):

void write_str(char s[], int n){
    int i;
    for(i=0;i<n;i++)
        putchar_unlocked(s[i]);
}

Я тестировал строку размером n и ровно n символов.
Но это самый медленный из трех, как мы можем видеть на этом графике количества записей на выходе по сравнению со временем в секундах:  График

Почему самый медленный?


person 648trindade    schedule 19.09.2015    source источник
comment
ось y = время (секунды); ось x = количество записей   -  person abligh    schedule 19.09.2015
comment
Как получить n? Жестко запрограммированная константа? Или используя strlen()? Кроме того, почему бы вам не использовать fputs() или fwrite()?   -  person user12205    schedule 19.09.2015
comment
@ DietmarKühl На самом деле заголовок графика довольно ясен (хотя он на португальском языке). Он читает время для записи N символьных массивов, поэтому я бы сказал, что N - это количество строк.   -  person Filipe Gonçalves    schedule 19.09.2015
comment
извините за язык графиков. Этот результат был достигнут путем печати той же строки размера x (фактически 30). Я получаю 100 раз выполнения для каждого случая, а затем вычисляю среднее значение и отображаю его на графике.   -  person 648trindade    schedule 19.09.2015
comment
Еще один глупый вопрос: я так понимаю, вы скомпилировали с оптимизацией?   -  person Dietmar Kühl    schedule 19.09.2015
comment
Неа. Нет флажков оптимизации.   -  person 648trindade    schedule 19.09.2015
comment
Профилирование без оптимизации - это совершенно глупо. Вы просите компилятор остановиться до того, как он завершит свою работу. Вы буквально просите его создать худший код и сделать вещи более подробными, чем ему нужно (и это может быть по-разному для разных конструкций). В результате ваши результаты бессмысленны!   -  person Lightness Races in Orbit    schedule 19.09.2015
comment
Да и тестирование ввода-вывода в любом случае сильно зависит от оборудования.   -  person Andrew Henle    schedule 19.09.2015
comment
Также обратите внимание, что вызывающий putchar_unlocked() должен заблокировать stdout. Вам, вероятно, следует обращаться к _3 _ / _ 4_ во время цикла for, если только тот, кто вызывает write_str(), не берет на себя эту ответственность.   -  person Michael Burr    schedule 19.09.2015
comment
@MichaelBurr это действительно нужно? мой код последовательный, однопоточный.   -  person 648trindade    schedule 19.09.2015
comment
@AndrewHenle: этот тест предназначен для определенной цели (соревнования по программированию), где все одинаково.   -  person 648trindade    schedule 19.09.2015
comment
@LightnessRacesinOrbit Я думаю, что оптимизация настроек компилятора снижает избыточность. Мой тестируемый код намеренно избыточен: заставляю машину делать одно и то же снова и снова. В этом случае различия должны вносить реализации функций верхнего уровня.   -  person 648trindade    schedule 19.09.2015
comment
Разве это не так просто, как переключение контекста?   -  person abligh    schedule 20.09.2015
comment
Соревнование по программированию? Будет сложно заменить ваш write_str( char s[], int n ) простым write( 1, s, ( size_t ) n );   -  person Andrew Henle    schedule 20.09.2015


Ответы (3)


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

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

Это в основном то, с чем вы сталкиваетесь, накладные расходы на системный вызов.

Повышение производительности putchar_unlocked по сравнению с putchar может быть значительным, но только между этими двумя функциями. Кроме того, в большинстве библиотек времени выполнения нет putchar_unlocked (я нахожу его в более старой документации MAC OS X, но не в Linux или Windows).

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

person JVene    schedule 19.09.2015
comment
Привет, @Jvene. Эти накладные расходы тоже возникают при вводе? Потому что getchar_unlocked побеждает scanf и cin во всех случаях обработки ввода. Я использую тестирование debian и gcc 5.2.1 (тоже g ++). - person 648trindade; 19.09.2015
comment
По идее, у scanf есть много работы, которой нет у getchar (любой разновидности). Посмотрите на источник scanf, чтобы понять, почему. Кроме того, имеет значение контекст и то, как вы оцениваете getchar по сравнению с cin. Вы вводите данные из канала в командной строке? Если да, то какая обработка строк используется? Вы можете обнаружить, что обязанности cin по обработке строк больше, чем вы определяете с помощью getchar. То есть контекст вызова функции OUTPUT полностью отличается от вызова функции INPUT, особенно такой, как scanf. - person JVene; 19.09.2015

Предполагая, что измерения времени для примерно 1 000 000 миллионов символов ниже порога измерения и записи в std::cout и stdout выполняются с использованием формы, использующей массовую запись (например, std::cout.write(str, size)), я предполагаю, что putchar_unlock() тратит большую часть своего времени на обновление некоторая часть структур данных в дополнение к помещению символа. Другие операции массовой записи будут копировать данные в буфер массово (например, с использованием memcpy()) и обновлять структуры данных внутри только один раз.

То есть коды будут выглядеть примерно так (это pidgeon-code, т.е. просто примерно показывает, что происходит; реальный код был бы, по крайней мере, немного сложнее):

int putchar_unlocked(int c) {
    *stdout->put_pointer++ = c;
    if (stdout->put_pointer != stdout->buffer_end) {
        return c;
    }
    int rc = write(stdout->fd, stdout->buffer_begin, stdout->put_pointer - stdout->buffer_begin);
    // ignore partial writes
    stdout->put_pointer = stdout->buffer_begin;
    return rc == stdout->buffer_size? c: EOF;
}

Вместо этого массовая версия кода делает что-то вроде этого (с использованием нотации C ++, поскольку легче быть разработчиком на C ++; опять же, это pidgeon-code):

int std::streambuf::write(char const* s, std::streamsize n) {
    std::lock_guard<std::mutex> guard(this->mutex);
    std::streamsize b = std::min(n, this->epptr() - this->pptr());
    memcpy(this->pptr(), s, b);
    this->pbump(b);
    bool success = true;
    if (this->pptr() == this->epptr()) {
        success = this->this->epptr() - this->pbase()
            != write(this->fd, this->pbase(), this->epptr() - this->pbase();
        // also ignoring partial writes
        this->setp(this->pbase(), this->epptr());
        memcpy(this->pptr(), s + b, n - b);
        this->pbump(n - b);
    }
    return success? n: -1;
}

Второй код может выглядеть немного сложнее, но выполняется только один раз для 30 символов. Большая часть проверок вынесена за пределы интересного. Даже если есть какая-то блокировка, она блокирует несогласованный мьютекс и не сильно препятствует обработке.

Особенно, когда не выполняется профилирование, цикл с использованием putchar_unlocked() не будет сильно оптимизирован. В частности, код не будет векторизован, что приводит к немедленному коэффициенту, по крайней мере, примерно 3, но, вероятно, даже ближе к 16 в фактическом цикле. Стоимость замка быстро уменьшится.

Кстати, просто для создания достаточно ровной площадки: помимо оптимизации вы также должны вызывать std::sync_with_stdio(false) при использовании стандартных потоковых объектов C ++.

person Dietmar Kühl    schedule 19.09.2015

Мое личное предположение состоит в том, что printf () делает это по частям и только время от времени должна проходить границу приложения / ядра для каждого фрагмента.

putchar_unlocked () делает это для каждого записанного байта.

person Russ Schultz    schedule 20.09.2015