Измерение времени выполнения программы с помощью счетчиков циклов

У меня есть путаница в этой конкретной строке -->

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

следующего кода:

void access_counter(unsigned *hi, unsigned *lo)
// Set *hi and *lo to the high and low order bits of the cycle
// counter. 
{
  asm("rdtscp; movl %%edx,%0; movl %%eax,%1"  // Read cycle counter
      : "=r" (*hi), "=r" (*lo)                // and move results to
      : /* No input */                        // the two outputs
      : "%edx", "%eax");
}

double get_counter()
// Return the number of cycles since the last call to start_counter.
{
    unsigned ncyc_hi, ncyc_lo;
    unsigned hi, lo, borrow;
    double result;

    /* Get cycle counter */
    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;
    if (result < 0) {
    fprintf(stderr, "Error: counter returns neg value: %.0f\n", result);
    }
    return result;
}

Я не могу понять, почему hi умножается на 2 ^ 30, а затем на 4? а потом низкий добавил к нему? Кто-нибудь, объясните, что происходит в этой строке кода. Я знаю, что содержит привет и низкий.


person Aisha Javed    schedule 16.04.2016    source источник
comment
Вы смотрели документы для rdtscp? Он возвращает 64-битное число. Младшие 32 бита в eax и старшие 32 бита в edx. В разумной реализации access_counter вернул бы 64-битное целое число. Почему это превращается в плавающую точку, я не могу себе представить.   -  person David Wohlferd    schedule 16.04.2016
comment
И пока я на нем, этот ассемблер написан неправильно. 1) Он изменяет ecx, не информируя об этом компилятор через вывод или затирание (очень плохо). 2) У него есть 2 ненужных оператора mov (трата времени и драгоценных регистров). Как насчет unsigned int a; unsigned long long b = __builtin_ia32_rdtscp(&a);? Если вы использовали 64-битное число для (очевидно, неопределенных?) cyc_lo и cyc_hi, это также упрощает вычитание нового и старого времени.   -  person David Wohlferd    schedule 17.04.2016


Ответы (1)


Короткий ответ:

Эта строка превращает 64-битное целое число, которое хранится в виде двух 32-битных значений, в число с плавающей запятой.

Почему код просто не использует 64-битное целое число? Что ж, gcc уже давно поддерживает 64-битные числа, но, по-видимому, этот код предшествует этому. В этом случае единственный способ поддерживать такие большие числа — поместить их в число с плавающей запятой.

Длинный ответ:

Во-первых, вам нужно понять, как работает rdtscp. Когда эта ассемблерная инструкция вызывается, она делает 2 вещи:

1) Устанавливает ecx в IA32_TSC_AUX MSR. По моему опыту, это обычно просто означает, что ecx устанавливается на ноль. 2) Устанавливает edx:eax в текущее значение счетчика отметок времени процессора. Это означает, что младшие 64 бита счетчика идут в eax, а старшие 32 бита — в edx.

Имея это в виду, давайте посмотрим на код. При вызове из get_counter access_counter помещает edx в ncyc_hi и eax в ncyc_lo. Затем get_counter сделает:

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

Что это делает?

Поскольку время хранится в двух разных 32-битных числах, если мы хотим узнать, сколько времени прошло, нам нужно немного поработать, чтобы найти разницу между старым временем и новым. Когда это сделано, результат сохраняется (опять же, с использованием 2 32-битных чисел) в hi / lo.

Что, наконец, подводит нас к вашему вопросу.

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

Если бы мы могли использовать 64-битные целые числа, преобразование двух 32-битных значений в одно 64-битное значение выглядело бы так:

unsigned long long result = hi; // put hi into the 64bit number.
result <<= 32;                  // shift the 32 bits to the upper part of the number
results |= low;                 // add in the lower 32bits.

Если вы не привыкли к смещению битов, возможно, такой взгляд на это поможет. Если lo = 1 и high = 2, то выражается в виде шестнадцатеричных чисел:

result = hi;   0x0000000000000002
result <<= 32; 0x0000000200000000
result |= low; 0x0000000200000001

Но если мы предположим, что компилятор не поддерживает 64-битные целые числа, это не сработает. Хотя числа с плавающей запятой могут содержать такие большие значения, они не поддерживают сдвиг. Итак, нам нужно найти способ сдвинуть «привет» влево на 32 бита, без использования сдвига влево.

Итак, сдвиг влево на 1 на самом деле такой же, как умножение на 2. Сдвиг влево на 2 аналогичен умножению на 4. Сдвиг влево на [опущено...] Сдвиг влево на 32 аналогичен умножению на 4 294 967 296.

По удивительному совпадению 4 294 967 296 == (1 ‹‹ 30) * 4.

Так зачем писать так сложно? Что ж, 4 294 967 296 — довольно большое число. Фактически, он слишком велик, чтобы поместиться в 32-битное целое число. Это означает, что если мы поместим его в наш исходный код, у компилятора, который не поддерживает 64-битные целые числа, могут возникнуть проблемы с выяснением того, как его обрабатывать. Написанный таким образом, компилятор может генерировать любые инструкции с плавающей запятой, которые могут ему понадобиться для работы с этим действительно большим числом.

Почему текущий код неверен:

Похоже, вариации этого кода уже давно бродят по интернету. Первоначально (я полагаю) access_counter был написан с использованием rdtsc вместо rdtscp. Я не буду пытаться описать разницу между ними (погуглите), кроме как указать, что rdtsc не устанавливает ecx, а rdtscp устанавливает. Тот, кто изменил rdtsc на rdtscp, по-видимому, не знал этого и не смог настроить встроенный ассемблер, чтобы отразить это. Хотя ваш код может работать нормально, несмотря на это, вместо этого он может делать что-то странное. Чтобы исправить это, вы можете сделать:

asm("rdtscp; movl %%edx,%0; movl %%eax,%1"  // Read cycle counter
  : "=r" (*hi), "=r" (*lo)                  // and move results to
  : /* No input */                          // the two outputs
  : "%edx", "%eax", "%ecx");

Хотя это будет работать, это не оптимально. Регистры являются ценным и дефицитным ресурсом на i386. Этот крошечный фрагмент использует 5 из них. С небольшой модификацией:

asm("rdtscp"  // Read cycle counter
  : "=d" (*hi), "=a" (*lo)
  : /* No input */
  : "%ecx");

Теперь у нас на 2 оператора сборки меньше, и мы используем только 3 регистра.

Но даже это не лучшее, что мы можем сделать. За (предположительно долгое) время, прошедшее с момента написания этого кода, gcc добавил поддержку 64-битных целых чисел и функцию для чтения tsc, поэтому вам вообще не нужно использовать asm:

unsigned int a;
unsigned long long result;

result =  __builtin_ia32_rdtscp(&a);

'a' - это (бесполезное?) значение, которое возвращалось в ecx. Вызов функции требует этого, но мы можем просто игнорировать возвращаемое значение.

Итак, вместо того, чтобы делать что-то подобное (что, как я предполагаю, делает ваш существующий код):

unsigned cyc_hi, cyc_lo;

access_counter(&cyc_hi, &cyc_lo);
// do something
double elapsed_time = get_counter(); // Find the difference between cyc_hi, cyc_lo and the current time

Мы сможем:

unsigned int a;
unsigned long long before, after;

before =  __builtin_ia32_rdtscp(&a);
// do something
after =  __builtin_ia32_rdtscp(&a);
unsigned long long elapsed_time = after - before;

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

Но для этого требуется относительно свежая версия gcc.

person David Wohlferd    schedule 17.04.2016