Почему первый printf занимает больше времени? - PullRequest
3 голосов
/ 02 сентября 2011

Я играл с высокоточными таймерами, и одним из моих первых тестов было использование rdtsc для измерения printf.Ниже моя тестовая программа с последующим выводом.Что я заметил, так это то, что при первом запуске printf на первом отпечатке он примерно в 25 раз дольше, чем на последующих.Почему это так?

#include <stdio.h>
#include <stdint.h>

// Sample code grabbed from wikipedia
__inline__ uint64_t rdtsc(void)
{
    uint32_t lo, hi;
    __asm__ __volatile__ (
            "xorl %%eax,%%eax \n        cpuid"
            ::: "%rax", "%rbx", "%rcx", "%rdx");
    __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
    return (uint64_t)hi << 32 | lo;
}

int main(int argc, const char *argv[])
{
    unsigned int i;
    uint64_t counter[10];
    uint64_t sum = 0;
    for (i = 0; i < 10; i++)
    {
        counter[i] = rdtsc();
        printf("Hello, world\n");
        counter[i] = rdtsc() - counter[i];
    }

    for (i = 0; i < 10; i++)
    {
        printf("counter[%d] = %lld\n", i, counter[i]);
        sum += counter[i];
    }
    printf("avg = %lld\n", sum/10);
    return 0;
}

И вывод:

Hello, world
Hello, world
Hello, world
Hello, world
Hello, world
Hello, world
Hello, world
Hello, world
Hello, world
Hello, world
counter[0] = 108165
counter[1] = 6375
counter[2] = 4388
counter[3] = 4388
counter[4] = 4380
counter[5] = 4545
counter[6] = 4215
counter[7] = 4290
counter[8] = 4237
counter[9] = 4320
avg = 14930

(Для справки, это было скомпилировано с gcc на OSX)

Ответы [ 5 ]

5 голосов
/ 02 сентября 2011

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

5 голосов
/ 02 сентября 2011

Я предполагаю, что при первом вызове printf ресурсы stdout не находятся в кеше, и вызов должен будет перенести его в кеш - следовательно, он медленнее.Для всех последующих вызовов кэш уже тёплый.

Второе возможное объяснение состоит в том, что, если это в Linux (может также применяться к OSX, я не уверен), программе необходимо установить потокориентации.(ASCII против UNICODE) Это делается при первом вызове функции, использующей этот поток, и является статическим до тех пор, пока поток не закроется.Я не знаю, сколько стоит установка этой ориентации, но это единовременная плата.

Пожалуйста, не стесняйтесь меня поправлять, если кто-то думает, что я совершенно неправ.

4 голосов
/ 02 сентября 2011

Это может быть какая-то ленивая инициализация .

4 голосов
/ 02 сентября 2011

Это около 50 микросекунд. Возможно проблема с кешированием? Слишком короткая, чтобы что-то делать с загрузкой с жесткого диска, но правдоподобная для загрузки большого объема библиотеки ввода-вывода C из ОЗУ.

1 голос
/ 02 сентября 2011

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

В качестве простого примера с аппаратной точки зрения рассмотрим два подхода к проектированию памяти: (1) существует единое хранилище памяти, и каждая операция занимает шестьдесят наносекунд; (2) существует несколько уровней кэша; извлечение слова, которое содержится в первом уровне кэша, займет одну наносекунду; слово, которого там нет, но которое хранится на втором уровне, займет пять; Слово, которого нет, но находится на третьем уровне, займет десять, а слово, которого там нет, займет шестьдесят. Если бы все обращения к памяти были абсолютно случайными, первый дизайн был бы не только проще, чем второй, но и работал бы лучше. Большинство обращений к памяти приводит к тому, что ЦП тратит десять наносекунд на поиск данных в кеше, прежде чем выходить из него и извлекать его из основной памяти. С другой стороны, если 80% обращений к памяти удовлетворяется первым уровнем кэша, 16% - вторым и 3% - третьим, то есть только один из ста должен выходить в основную память, тогда среднее время для этих обращений к памяти будет 2,5 нс. Это в среднем в сорок раз быстрее, чем простая система памяти.

Даже если целая программа предварительно загружена с диска, при первом запуске такой подпрограммы, как «printf», ни она, ни любые требуемые ей данные, скорее всего, не попадут в кэш любого уровня. Следовательно, медленный доступ к памяти потребуется при первом запуске. С другой стороны, после того как код и большая часть его необходимых данных будут кэшированы, будущие исполнения будут намного быстрее. Если повторное выполнение фрагмента кода происходит, пока он все еще находится в самом быстром кеше, разница в скорости может легко составить порядок величины. Оптимизация для быстрого случая во многих случаях приведет к тому, что однократное выполнение кода будет намного медленнее, чем это было бы в противном случае (даже в большей степени, чем предлагается в приведенном выше примере), но поскольку многие процессоры тратят большую часть своего времени на выполнение небольших фрагментов кода в миллионы или миллиарды раз, ускорения, полученные в этих ситуациях, намного перевешивают любое замедление при выполнении подпрограмм, которые выполняются только один раз.

...