Почему clock_gettime так неустойчив?

Введение

  • В разделе "Старый вопрос" содержится первоначальный вопрос (добавлено дополнительное исследование и заключение).

  • Перейти к разделу Дальнейшее исследование ниже для детального сравнения различных методов синхронизации (rdtsc, clock_gettime и QueryThreadCycleTime).

  • Я считаю, что ошибочное поведение CGT можно отнести либо к багги-ядру, либо к багги-процессору (см. раздел "Заключение" ).

  • Код, используемый для тестирования, находится в нижней части этого вопроса (см. раздел Приложение).

  • Извините за длину.


Старый вопрос

Короче: я использую clock_gettime для измерения времени выполнения многих сегментов кода. Я испытываю очень несовместимые измерения между отдельными прогонами. Метод имеет чрезвычайно высокое стандартное отклонение по сравнению с другими методами (см. Пояснение ниже).

Вопрос: Есть ли причина, по которой clock_gettime дает такие противоречивые измерения по сравнению с другими методами? Есть ли альтернативный метод с тем же разрешением, который учитывает время простоя потока?

Объяснение. Я пытаюсь профилировать несколько небольших частей кода C. Время выполнения каждого из сегментов кода составляет не более нескольких микросекунд. За один проход каждый сегмент кода будет выполняться несколько сотен раз, что производит измерения runs × hundreds.

Я также должен измерять только время, в течение которого поток фактически проводит выполнение (поэтому rdtsc не подходит). Мне также требуется высокое разрешение (поэтому times не подходит).

Я пробовал следующие методы:

  • rdtsc (в Linux и Windows),

  • clock_gettime (с 'CLOCK_THREAD_CPUTIME_ID', в Linux) и

  • QueryThreadCycleTime (в Windows).

Методология. Анализ проводился на 25 прогонах. В каждом прогоне отдельные сегменты кода повторяют 101 раз. Поэтому у меня 2525 измерений. Затем я смотрю на гистограмму измерений, а также вычисляю некоторые основные вещи (например, среднее значение, std.dev., Медиана, режим, мин и макс).

Я не представляю, как я измерил "сходство" трех методов, но это просто включало базовое сравнение доли времени, проведенного в каждом сегменте кода ( "пропорция" означает, что времена нормализованы). Затем я смотрю на чистые различия в этих пропорциях. Это сравнение показало, что все "rdtsc", "QTCT" и "CGT" измеряют одинаковые пропорции при усреднении по 25 прогонам. Однако приведенные ниже результаты показывают, что "CGT" имеет очень большое стандартное отклонение. Это делает его непригодным для использования в моем случае использования.

Результаты

Сравнение clock_gettime с rdtsc для одного и того же сегмента кода (25 прогонов 101 измерения = 2525 отсчетов):

  • clock_gettime

    • 1881 измерений 11 нс,
    • 595 измерений были (распределены почти нормально) между 3369 и 3414 нс,
    • 2 измерения 11680 нс,
    • 1 измерение 1506022 нс и
    • остальное находится между 900 и 5000 нс.

    • Мин: 11 нс

    • Макс: 1506022 нс
    • Значение: 1471.862 нс
    • Медиана: 11 нс
    • Режим: 11 нс
    • Stddev: 29991.034
  • rdtsc (обратите внимание: во время этого прогона не было переключений контекста, но если это произойдет, обычно это приводит к одному измерению 30000 тиков или около того):

    • 1178 измерений между 274 и 325 тиками,
    • 306 измерений между 326 и 375 тиками,
    • 910 измерений между 376 и 425 тиками,
    • 129 измерений между 426 и 990 тиками,
    • 1 измерение 1240 тиков и
    • 1 измерение 1256 тиков.

    • Мин: 274 тиков

    • Макс: 1256 тиков
    • Значение: 355.806 тиков
    • Медиана: 333 тика
    • Режим: 376 тиков
    • Stddev: 83.896

Обсуждение

  • rdtsc дает очень похожие результаты как для Linux, так и для Windows. Он имеет приемлемое стандартное отклонение - он на самом деле довольно устойчив/стабилен. Однако он не учитывает время простоя потока. Таким образом, переключатели контекста делают измерения неустойчивыми (в Windows я наблюдал это довольно часто: сегмент кода со средним числом 1000 тиков или около того будет принимать ~ 30000 тиков каждый раз, а затем - определенно из-за предварительного извлечения).

  • QueryThreadCycleTime дает очень последовательные измерения - т.е. значительно меньшее стандартное отклонение по сравнению с rdtsc. Когда никаких переключений контекста не происходит, этот метод почти идентичен rdtsc.

  • clock_gettime, с другой стороны, создает чрезвычайно противоречивые результаты (не только между прогонами, но и между измерениями). Стандартные отклонения являются экстремальными (по сравнению с rdtsc).

Я надеюсь, что статистика в порядке. Но что может быть причиной такого расхождения в измерениях между этими двумя методами? Конечно, есть кэширование, миграция CPU/core и другие вещи. Но ничто из этого не должно отвечать за любые такие различия между "rdtsc" и "clock_gettime". Что происходит?


Дальнейшее исследование

Я исследовал это немного дальше. Я сделал две вещи:

  • Измерение накладных расходов при вызове clock_gettime(CLOCK_THREAD_CPUTIME_ID, &t) (см. код 1 в Приложении) и

  • в обычном цикле, называемом clock_gettime, и сохранил показания в массиве (см. код 2 в Приложении). Я измеряю времена дельта (разницу в последовательных временах измерения, которые должны немного соответствовать служебным запросам вызова clock_gettime).

Я измерил его на двух разных компьютерах с двумя разными версиями ядра Linux:

ВКТ

  • CPU: Core 2 Duo L9400 @1,86 ГГц

    Ядро: Linux 2.6.40-4.fc15.i686 # 1 SMP Пт Июл 29 18:54:39 ​​UTC 2011 i686 i686 i386

    Результаты

    • Оценочные накладные расходы clock_gettime: между 690-710 нс
    • Дельта раз:

      • В среднем: 815.22 нс
      • Медиана: 713 нс
      • Режим: 709 нс
      • Мин.: 698 нс
      • Максимум: 23359 нс
      • Гистограмма (левые диапазоны имеют частоты 0):

              Range       |  Frequency
        ------------------+-----------
          697 < x ≤ 800   ->     78111  <-- cached?
          800 < x ≤ 1000  ->     16412
         1000 < x ≤ 1500  ->         3
         1500 < x ≤ 2000  ->      4836  <-- uncached?
         2000 < x ≤ 3000  ->       305
         3000 < x ≤ 5000  ->       161
         5000 < x ≤ 10000 ->       105
        10000 < x ≤ 15000 ->        53
        15000 < x ≤ 20000 ->         8
        20000 < x         ->         5
        
  • CPU: 4-процессорный двухпроцессорный процессор AMD Opteron 275

    Ядро: Linux 2.6.26-2-amd64 # 1 SMP Sun Jun 20 20:16:30 UTC 2010 x86_64 GNU/Linux

    Результаты

    • Оценочные накладные расходы clock_gettime: между 279-283 нс
    • Дельта раз:

      • В среднем: 320.00
      • Медиана: 1
      • Режим: 1
      • Мин: 1
      • Макс: 3495529
      • Гистограмма (левые диапазоны имеют частоты 0):

              Range         |  Frequency
        --------------------+-----------
                  x ≤ 1     ->     86738  <-- cached?
            282 < x ≤ 300   ->     13118  <-- uncached?
            300 < x ≤ 440   ->        78
           2000 < x ≤ 5000  ->        52
           5000 < x ≤ 30000 ->         5
        3000000 < x         ->         8
        

RDTSC

Связанный код rdtsc_delta.c и rdtsc_overhead.c.

  • CPU: Core 2 Duo L9400 @1,86 ГГц

    Ядро: Linux 2.6.40-4.fc15.i686 # 1 SMP Пт Июл 29 18:54:39 ​​UTC 2011 i686 i686 i386

    Результаты

    • Оценочные накладные расходы: между 39-42 тиками
    • Дельта раз:

      • В среднем: 52.46 тиков
      • Медиана: 42 тика
      • Режим: 42 тика
      • Мин: 35 тиков
      • Макс: 28700 тиков
      • Гистограмма (левые диапазоны имеют частоты 0):

              Range       |  Frequency
        ------------------+-----------
           34 < x ≤ 35    ->     16240  <-- cached?
           41 < x ≤ 42    ->     63585  <-- uncached? (small difference)
           48 < x ≤ 49    ->     19779  <-- uncached?
           49 < x ≤ 120   ->       195
         3125 < x ≤ 5000  ->       144
         5000 < x ≤ 10000 ->        45
        10000 < x ≤ 20000 ->         9
        20000 < x         ->         2
        
  • CPU: 4-процессорный двухпроцессорный процессор AMD Opteron 275

    Ядро: Linux 2.6.26-2-amd64 # 1 SMP Sun Jun 20 20:16:30 UTC 2010 x86_64 GNU/Linux

    Результаты

    • Оценочные накладные расходы: от 13,7 до 17,0 тиков
    • Дельта раз:

      • В среднем: 35,44 тика
      • Медиана: 16 тиков
      • Режим: 16 тиков
      • Мин: 14 тиков
      • Макс: 16372 тиков
      • Гистограмма (левые диапазоны имеют частоты 0):

              Range       |  Frequency
        ------------------+-----------
           13 < x ≤ 14    ->       192
           14 < x ≤ 21    ->     78172  <-- cached?
           21 < x ≤ 50    ->     10818
           50 < x ≤ 103   ->     10624  <-- uncached?
         5825 < x ≤ 6500  ->        88
         6500 < x ≤ 8000  ->        88
         8000 < x ≤ 10000 ->        11
        10000 < x ≤ 15000 ->         4
        15000 < x ≤ 16372 ->         2
        

QTCT

Связанный код qtct_delta.c и qtct_overhead.c.

  • CPU: Core 2 6700 @2,66 ГГц

    Ядро: 64-разрядная версия Windows 7

    Результаты

    • Оценочные накладные расходы: между 890-940 тиками
    • Дельта раз:

      • В среднем: 1057,30 тиков
      • Медиана: 890 тиков
      • Режим: 890 тиков
      • Мин.: 880 тиков
      • Макс: 29400 тиков
      • Гистограмма (левые диапазоны имеют частоты 0):

              Range       |  Frequency
        ------------------+-----------
          879 < x ≤ 890   ->     71347  <-- cached?
          895 < x ≤ 1469  ->       844
         1469 < x ≤ 1600  ->     27613  <-- uncached?
         1600 < x ≤ 2000  ->        55
         2000 < x ≤ 4000  ->        86
         4000 < x ≤ 8000  ->        43
         8000 < x ≤ 16000 ->        10
        16000 < x         ->         1
        

Заключение

Я считаю, что ответ на мой вопрос будет ошибкой на моей машине (той, которая имеет процессоры AMD со старым ядром Linux).

Результаты CGT машины AMD со старым ядром показывают некоторые крайние показания. Если мы посмотрим на времена дельты, мы увидим, что наиболее частая дельта равна 1 нс. Это означает, что вызов clock_gettime занял меньше, чем наносекунда! Более того, он также произвел ряд необычайно больших дельт (более 3000000 нс)! Это кажется ошибочным поведением. (Возможно, неучтенные миграции ядра?)

Примечания:

  • Накладные расходы CGT и QTCT довольно большие.

  • Также сложно учесть их накладные расходы, потому что кэширование процессора, по-видимому, имеет большое значение.

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


Приложение

Код 1: clock_gettime_overhead.c

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

/* Compiled & executed with:

    gcc clock_gettime_overhead.c -O0 -lrt -o clock_gettime_overhead
    ./clock_gettime_overhead 100000
*/

int main(int argc, char **args) {
    struct timespec tstart, tend, dummy;
    int n, N;
    N = atoi(args[1]);
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &tstart);
    for (n = 0; n < N; ++n) {
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &dummy);
    }
    clock_gettime(CLOCK_THREAD_CPUTIME_ID, &tend);
    printf("Estimated overhead: %lld ns\n",
            ((int64_t) tend.tv_sec * 1000000000 + (int64_t) tend.tv_nsec
                    - ((int64_t) tstart.tv_sec * 1000000000
                            + (int64_t) tstart.tv_nsec)) / N / 10);
    return 0;
}

Код 2: clock_gettime_delta.c

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

/* Compiled & executed with:

    gcc clock_gettime_delta.c -O0 -lrt -o clock_gettime_delta
    ./clock_gettime_delta > results
*/

#define N 100000

int main(int argc, char **args) {
    struct timespec sample, results[N];
    int n;
    for (n = 0; n < N; ++n) {
        clock_gettime(CLOCK_THREAD_CPUTIME_ID, &sample);
        results[n] = sample;
    }
    printf("%s\t%s\n", "Absolute time", "Delta");
    for (n = 1; n < N; ++n) {
        printf("%lld\t%lld\n",
               (int64_t) results[n].tv_sec * 1000000000 + 
                   (int64_t)results[n].tv_nsec,
               (int64_t) results[n].tv_sec * 1000000000 + 
                   (int64_t) results[n].tv_nsec - 
                   ((int64_t) results[n-1].tv_sec * 1000000000 + 
                        (int64_t)results[n-1].tv_nsec));
    }
    return 0;
}

Код 3: rdtsc.h

static uint64_t rdtsc() {
#if defined(__GNUC__)
#   if defined(__i386__)
    uint64_t x;
    __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x));
    return x;
#   elif defined(__x86_64__)
    uint32_t hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ((uint64_t)lo) | ((uint64_t)hi << 32);
#   else
#       error Unsupported architecture.
#   endif
#elif defined(_MSC_VER)
    return __rdtsc();
#else
#   error Other compilers not supported...
#endif
}

Код 4: rdtsc_delta.c

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

/* Compiled & executed with:

    gcc rdtsc_delta.c -O0 -o rdtsc_delta
    ./rdtsc_delta > rdtsc_delta_results

Windows:

    cl -Od rdtsc_delta.c
    rdtsc_delta.exe > windows_rdtsc_delta_results
*/

#define N 100000

int main(int argc, char **args) {
    uint64_t results[N];
    int n;
    for (n = 0; n < N; ++n) {
        results[n] = rdtsc();
    }
    printf("%s\t%s\n", "Absolute time", "Delta");
    for (n = 1; n < N; ++n) {
        printf("%lld\t%lld\n", results[n], results[n] - results[n-1]);
    }
    return 0;
}

Код 5: rdtsc_overhead.c

#include <time.h>
#include <stdio.h>
#include <stdint.h>
#include "rdtsc.h"

/* Compiled & executed with:

    gcc rdtsc_overhead.c -O0 -lrt -o rdtsc_overhead
    ./rdtsc_overhead 1000000 > rdtsc_overhead_results

Windows:

    cl -Od rdtsc_overhead.c
    rdtsc_overhead.exe 1000000 > windows_rdtsc_overhead_results
*/

int main(int argc, char **args) {
    uint64_t tstart, tend, dummy;
    int n, N;
    N = atoi(args[1]);
    tstart = rdtsc();
    for (n = 0; n < N; ++n) {
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
        dummy = rdtsc();
    }
    tend = rdtsc();
    printf("%G\n", (double)(tend - tstart)/N/10);
    return 0;
}

Код 6: qtct_delta.c

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

/* Compiled & executed with:

    cl -Od qtct_delta.c
    qtct_delta.exe > windows_qtct_delta_results
*/

#define N 100000

int main(int argc, char **args) {
    uint64_t ticks, results[N];
    int n;
    for (n = 0; n < N; ++n) {
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        results[n] = ticks;
    }
    printf("%s\t%s\n", "Absolute time", "Delta");
    for (n = 1; n < N; ++n) {
        printf("%lld\t%lld\n", results[n], results[n] - results[n-1]);
    }
    return 0;
}

Код 7: qtct_overhead.c

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

/* Compiled & executed with:

    cl -Od qtct_overhead.c
    qtct_overhead.exe 1000000
*/

int main(int argc, char **args) {
    uint64_t tstart, tend, ticks;
    int n, N;
    N = atoi(args[1]);
    QueryThreadCycleTime(GetCurrentThread(), &tstart);
    for (n = 0; n < N; ++n) {
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
    }
    QueryThreadCycleTime(GetCurrentThread(), &tend);
    printf("%G\n", (double)(tend - tstart)/N/10);
    return 0;
}
#include <stdio.h>
#include <stdint.h>
#include <Windows.h>

/* Compiled & executed with:

    cl -Od qtct_overhead.c
    qtct_overhead.exe 1000000
*/

int main(int argc, char **args) {
    uint64_t tstart, tend, ticks;
    int n, N;
    N = atoi(args[1]);
    QueryThreadCycleTime(GetCurrentThread(), &tstart);
    for (n = 0; n < N; ++n) {
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
        QueryThreadCycleTime(GetCurrentThread(), &ticks);
    }
    QueryThreadCycleTime(GetCurrentThread(), &tend);
    printf("%G\n", (double)(tend - tstart)/N/10);
    return 0;
}

Ответ 1

Ну, как CLOCK_THREAD_CPUTIME_ID реализуется с помощью rdtsc, он, вероятно, будет страдать от тех же проблем, что и он. На странице руководства для clock_gettime говорится:

Часы CLOCK_PROCESS_CPUTIME_ID и CLOCK_THREAD_CPUTIME_IDреализованы на многих платформах с использованием таймеров от CPU (TSC on i386, AR.ITC на Itanium). Эти регистры могут различаться между ЦП и как следствие, эти часы могут возвращать фиктивные результаты, если процесс переносится в другой процессор.

Как это звучит, это может объяснить ваши проблемы? Может быть, вы должны заблокировать свой процесс на одном CPU, чтобы получить стабильные результаты?

Ответ 2

Когда у вас сильно искаженное распределение, которое не может пойти отрицательно, вы увидите большие расхождения между средним, медианным и режимом. Стандартное отклонение не имеет смысла для такого распределения.

Как правило, хорошая идея - преобразовать его в лог файл. Это сделает его "более нормальным".