Вычисление частоты процессора в C с помощью RDTSC всегда возвращает 0

Следующий фрагмент кода был предоставлен нам от нашего инструктора, чтобы мы могли измерить производительность алгоритмов:

#include <stdio.h>
#include <unistd.h>

static unsigned cyc_hi = 0, cyc_lo = 0;

static void access_counter(unsigned *hi, unsigned *lo) {
    asm("rdtsc; movl %%edx,%0; movl %%eax,%1"
    : "=r" (*hi), "=r" (*lo)
    : /* No input */
    : "%edx", "%eax");
}

void start_counter() {
    access_counter(&cyc_hi, &cyc_lo);
}

double get_counter() {
    unsigned ncyc_hi, ncyc_lo, hi, lo, borrow;
    double result;

    access_counter(&ncyc_hi, &ncyc_lo);

    lo = ncyc_lo - cyc_lo;
    borrow = lo > ncyc_lo;
    hi = ncyc_hi - cyc_hi - borrow;

    result = (double) hi * (1 << 30) * 4 + lo;

    return result;
}

Однако мне нужно, чтобы этот код был переносимым для машин с разными частотами процессора. Для этого я пытаюсь вычислить частоту процессора машины, на которой выполняется код следующим образом:

int main(void)
{
    double c1, c2;

    start_counter();

    c1 = get_counter();
    sleep(1);
    c2 = get_counter();

    printf("CPU Frequency: %.1f MHz\n", (c2-c1)/1E6);
    printf("CPU Frequency: %.1f GHz\n", (c2-c1)/1E9);

    return 0;
}

Проблема заключается в том, что результат всегда равен 0, и я не могу понять, почему. Я запускаю Linux (Arch) в качестве гостевой версии на VMware.

На другом компьютере (MacBook) он работает в некоторой степени; Я имею в виду, что результат больше 0, но он переменный, потому что частота процессора не фиксирована (мы пытались ее исправить, но по какой-то причине мы не можем это сделать). У него есть другая машина, на которой работает Linux (Ubuntu) в качестве хоста, и она также сообщает 0. Это исключает проблему на виртуальной машине, которая, как я думал, сначала была проблемой.

Любые идеи, почему это происходит и как я могу это исправить?

Ответ 1

Хорошо, так как другой ответ не помог, я попытаюсь объяснить более подробно. Проблема в том, что современный процессор может выполнять инструкции не по порядку. Ваш код начинается как-то вроде:

rdtsc
push 1
call sleep
rdtsc

Современные процессоры не обязательно выполняют инструкции в их первоначальном порядке. Несмотря на ваш первоначальный заказ, CPU (в основном) можно выполнить так же, как:

rdtsc
rdtsc
push 1
call sleep

В этом случае ясно, почему разница между двумя rdtsc будет (по крайней мере, очень близка к нулю). Чтобы этого не произошло, вам нужно выполнить инструкцию о том, что CPU никогда не будет переупорядочиваться, чтобы выполнить не в порядке, Наиболее распространенная инструкция для этого - CPUID. Другой ответ, который я связывал, должен (если память обслуживается) начинать грубо оттуда, о шагах, необходимых для правильного/эффективного использования CPUID для этой задачи.

Конечно, возможно, что Tim Post был прав, и вы также видите проблемы из-за виртуальной машины. Тем не менее, поскольку он стоит прямо сейчас, нет гарантии, что ваш код будет работать правильно даже на реальном оборудовании.

Отредактируйте: почему код будет работать: ну, во-первых, тот факт, что инструкции могут быть выполнены не в порядке, не гарантирует, что они будут. Во-вторых, возможно, что (по крайней мере, некоторые реализации) sleep содержат инструкции по сериализации, которые предотвращают перегруппировку rdtsc, а другие не могут (или могут содержать их, но только выполнять их под конкретными (но неуказанными) обстоятельства).

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

Ответ 2

Я не могу точно сказать, что именно не так с вашим кодом, но вы делаете довольно ненужную работу для такой простой инструкции. Я рекомендую вам существенно упростить код rdtsc. Вам не нужно делать 64-битную математику, которая несет вас, и вам не нужно сохранять результат этой операции как двойной. Вам не нужно использовать отдельные выходы в вашем встроенном asm, вы можете сказать GCC использовать eax и edx.

Вот очень упрощенная версия этого кода:

#include <stdint.h>

uint64_t rdtsc() {
    uint64_t ret;

# if __WORDSIZE == 64
    asm ("rdtsc; shl $32, %%rdx; or %%rdx, %%rax;"
        : "=A"(ret)
        : /* no input */
        : "%edx"
    );
#else
    asm ("rdtsc" 
        : "=A"(ret)
    );
#endif
    return ret;
}

Также вы должны подумать о том, чтобы распечатать значения, которые вы получаете из этого, чтобы вы могли видеть, что вы получаете 0s или что-то еще.

Ответ 3

Что касается VMWare, посмотрите время сохранения спецификации (PDF Link), а также этот поток. Инструкции TSC (в зависимости от гостевой ОС):

  • Передано непосредственно реальному оборудованию (PV guest)
  • Циклы подсчета во время выполнения виртуальной машины на главном процессоре (Windows/etc)

Обратите внимание, что в # 2 во время выполнения VM на главном процессоре. То же самое было бы для Xen, если я правильно вспомню. В сущности, вы можете ожидать, что код будет работать, как ожидалось, у паравиртуализированного гостя. Если эмулировать, его совершенно необоснованно ожидать от аппаратного обеспечения, как согласованность.

Ответ 4

hmmm Я не уверен, но я подозреваю, что проблема может быть внутри этой строки:

result = (double) hi * (1 < 30) * 4 + lo;

Я подозрительный, если вы можете безопасно выполнять такие огромные умножения в "unsigned"... не так ли часто 32-битное число?... просто тот факт, что вы не могли безопасно размножаться на 2 ^ 32 и должны были добавить его в качестве дополнительного "* 4", добавленного к 2 ^ 30, в конце уже намекает на эту возможность... вам может потребоваться преобразуйте каждую подкомпонента hi и lo в двойную (а не одну в самом конце) и произведите умножение, используя два удвоения