Как я могу эффективно рассчитать время выполнения функции длиной всего в несколько циклов? - PullRequest
2 голосов
/ 27 мая 2020

Я пытаюсь провести некоторые сравнения различных методов вычисления точечных произведений с использованием SSE Intrinsics, но, поскольку методы длится всего несколько циклов, мне приходится запускать инструкции триллионы раз, чтобы это заняло больше, чем крошечный доли секунды. Единственная проблема заключается в том, что gcc с флагом -O3 «оптимизирует» мой main метод до бесконечного l oop.

Мой код

#include <immintrin.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <inttypes.h>
#define NORMAL 0

struct _Vec3 {
    float x;
    float y;
    float z;
    float w;
};

typedef struct _Vec3 Vec3;

__m128 singleDot(__m128 a, __m128 b) {
    return _mm_dp_ps(a, b, 0b00001111);
}

int main(int argc, char** argv) {
    for (uint16_t j = 0; j < (1L << 16); j++) {
        for (uint64_t i = 0; i < (1L << 62); i++) {
            Vec3 a = {i, i + 0.5, i + 1, 0.0};
            Vec3 b = {i, i - 0.5, i - 1, 0.0};
            #if NORMAL
            float ans = normalDot(a, b); // naive implementation
            #else
            // float _c[4] = {a.x, a.y, a.z, 0.0};
            // float _d[4] = {b.x, b.y, b.z, 0.0};
            __m128 c = _mm_load_ps((float*)&a);
            __m128 d = _mm_load_ps((float*)&b);
            __m128 ans = singleDot(c, d);
            #endif
        }
    }
}

но когда я компилирую с gcc -std=c11 -march=native -O3 main.c и запускаю objdump -d, он превращается в main в

0000000000400400 <main>:
  400400:   eb fe                   jmp    400400 <main>

есть ли альтернатива для разных подходов по времени?

Ответы [ 2 ]

2 голосов
/ 27 мая 2020

Даже после исправления опечатки uint16_t вместо uint64_t, которая делает ваш l oop бесконечным, фактическая работа все равно будет оптимизирована, потому что результат ничем не используется.

Вы можете использовать Google Benchmark's DoNotOptimize, чтобы предотвратить оптимизацию неиспользованного результата ans. например, такие функции, как «Escape» и «Clobber», которые в этом Q&A спрашивают о . Это работает в G CC, и этот вопрос связан с релевантным видео на YouTube из выступления разработчика clang CppCon.

Другой худший способ - присвоить результат переменной volatile. Но имейте в виду, что исключение общих подвыражений может по-прежнему оптимизировать более ранние части вычисления, независимо от того, используете ли вы volatile или встроенный asm-макрос, чтобы убедиться, что компилятор где-то материализует фактический конечный результат. Микробенчмаркинг - это сложно. Вам нужен компилятор, чтобы выполнять точно такой же объем работы, который мог бы произойти в реальном сценарии использования, но не больше.

См. Идиоматия c способ оценки производительности? за это и многое другое.


Помните, что именно вы здесь измеряете.

Вероятно, куча l oop накладные расходы и, возможно, переадресация хранилища в зависимости от того, векторизует компилятор эти инициализаторы или нет , но даже если это так; преобразование целого числа в FP и добавление 2x SIMD FP сопоставимы по стоимости dpps с точки зрения стоимости пропускной способности. (Это то, что вы измеряете, а не задержку; разница имеет большое значение для ЦП с выполнением вне очереди, в зависимости от контекста вашего реального варианта использования).

Производительность не равна 1 -размерный в масштабе пары инструкций. Привязка повторения l oop к некоторой работе может измерять пропускную способность или задержку, в зависимости от того, делаете ли вы ввод зависимым от предыдущего вывода (цепочка зависимостей al oop). Но если ваша работа ограничивается пропускной способностью внешнего интерфейса, то важной частью являются накладные расходы l oop. Кроме того, вы можете столкнуться с эффектами из-за того, что машинный код вашего l oop совпадает с 32-байтовыми границами для кэша uop.

Для чего-то такого короткого и простого анализа stati c является обычно хорошо. Подсчитайте количество мопов для внешнего интерфейса и порты в серверной части и проанализируйте задержку. Какие соображения go при прогнозировании задержки для операций на современных суперскалярных процессорах и как я могу рассчитать их вручную? . LLVM-MCA может сделать это за вас, так же как и IACA. Вы также можете выполнять измерения как часть своего реального l oop, в котором используются скалярные произведения.

См. Также RDTSCP в NASM всегда возвращает одно и то же значение для обсуждения того, что вы можете измерить в одна инструкция.


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

Текущие процессоры x86 могут l oop в лучшем случае одна итерация за такт для крошечного l oop. Невозможно написать al oop, который работает быстрее этого. 4 миллиарда итераций (в asm) займут как минимум целую секунду на процессоре с тактовой частотой 4 ГГц.

Конечно, оптимизирующий C компилятор может развернуть ваш l oop и выполнять столько итераций исходного кода, сколько он хочет за прыжок в асм.

2 голосов
/ 27 мая 2020

Это потому, что это:

for (uint16_t j = 0; j < (1L << 16); j++) {

- это infinte l oop - максимальное значение для uint16_t составляет 65535 (2 16 -1), после чего он будет вернуться к 0. Таким образом, тест всегда будет верным.

...