Можем ли мы победить компилятор? - PullRequest
1 голос
/ 10 июля 2019

Сегодня я писал Пул Распределитель, когда у меня возник вопрос:
Возможно ли победить компилятор?

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

Итак, я придумал очень простой BytePool:

template <size_t PoolSize>
class BytePool
{
public:
    template <typename T>
    T& At(size_t p_index)
    {
        return (T&)m_data[p_index * sizeof(T)];
    }

private:
    std::byte m_data[PoolSize];
};

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

. Чтобы манипулировать этим массивом, я создал макрос:

#define is(type, slot) bytePool.At<type>(slot)

Этот макрос позволяет мне написать: #define a is (int, 0x0000), например, a - псевдопеременная, которая указывает на bytePool[sizeof(int) * 0x0000].

. Используя этот макрос, я сделал простой фрагменткод, который выполняет основные операции с некоторыми числами (некоторые определены во время компиляции, а некоторые во время выполнения, такие как b и c):

BytePool<sizeof(int) * 6> bytePool;

#define is(type, slot) bytePool.At<type>(slot)

#define a is (int, 0x0000)
#define b is (int, 0x0001)
#define c is (int, 0x0002)
#define d is (int, 0x0003)
#define e is (int, 0x0004)
#define f is (int, 0x0004)

a = 0;
b = (int)time(nullptr);
c = (int)__rdtsc();
d = 2 * b;
e = c - 3;
f = 18 ^ 2;

a = ~(b * c) * d + e / f;

#undef a
#undef b
#undef c
#undef d
#undef e
#undef f

Fun!Этот код выглядит так, как будто я вручную назначил слоты памяти своим переменным.

Эквивалентом без использования ByteAllocator будет:

int a;
int b;
int c;
int d;
int e;
int f;

a = 0;
b = (int)time(nullptr);
c = (int)__rdtsc();
d = 2 * b;
e = c - 3;
f = 18 ^ 2;

a = ~(b * c) * d + e / f;

Вопрос, который я задал себе в этот момент:
Какой из этих подходов лучше?

  • Выделение sizeof (int) в стеке 6 раз
  • Allocation sizeof (int) * 6 onстек 1 раз

Естественно, я был уверен, что когда-то выделение памяти было быстрее.Поэтому я предположил, что мой подход BytePool был быстрее.

Теперь давайте послушаем компилятор.Я написал некоторый код для теста:

#include <iostream>
#include <intrin.h>
#include <ctime>

template <size_t PoolSize>
class BytePool
{
public:
    template <typename T>
    T& At(size_t p_index)
    {
        return (T&)m_data[p_index * sizeof(T)];
    }

private:
    std::byte m_data[PoolSize];
};

void Stack()
{
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;

    a = 0;
    b = (int)time(nullptr);
    c = (int)__rdtsc();
    d = 2 * b;
    e = c - 3;
    f = 18 ^ 2;

    a = ~(b * c) * d + e / f;
}

void Pool()
{
    BytePool<sizeof(int) * 6> bytePool;

    #define is(type, slot) bytePool.At<type>(slot)

    #define a is (int, 0x0000)
    #define b is (int, 0x0001)
    #define c is (int, 0x0002)
    #define d is (int, 0x0003)
    #define e is (int, 0x0004)
    #define f is (int, 0x0004)

    a = 0;
    b = (int)time(nullptr);
    c = (int)__rdtsc();
    d = 2 * b;
    e = c - 3;
    f = 18 ^ 2;

    a = ~(b * c) * d + e / f;

    #undef a
    #undef b
    #undef c
    #undef d
    #undef e
    #undef f
}

void FastPool()
{
    int fastBytePool[6];

    #define a   *(fastBytePool)
    #define b   *(fastBytePool + 0x0001)
    #define c   *(fastBytePool + 0x0002)
    #define d   *(fastBytePool + 0x0003)
    #define e   *(fastBytePool + 0x0004)
    #define f   *(fastBytePool + 0x0005)

    a = 0;
    b = (int)time(nullptr);
    c = (int)__rdtsc();
    d = 2 * b;
    e = c - 3;
    f = 18 ^ 2;

    a = ~(b * c) * d + e / f;

    #undef a
    #undef b
    #undef c
    #undef d
    #undef e
    #undef f
}

void FastHeapPool()
{
    int* fastBytePool = new int[6];

    #define a   *(fastBytePool)
    #define b   *(fastBytePool + 0x0001)
    #define c   *(fastBytePool + 0x0002)
    #define d   *(fastBytePool + 0x0003)
    #define e   *(fastBytePool + 0x0004)
    #define f   *(fastBytePool + 0x0005)

    a = 0;
    b = (int)time(nullptr);
    c = (int)__rdtsc();
    d = 2 * b;
    e = c - 3;
    f = 18 ^ 2;

    a = ~(b * c) * d + e / f;

    #undef a
    #undef b
    #undef c
    #undef d
    #undef e
    #undef f

    delete[] fastBytePool;
}

size_t Benchmark(void (p_function)(), size_t p_iterations)
{
    size_t cycleSum = 0;

    for (size_t it = 0; it < p_iterations; ++it)
    {
        size_t startCycles = __rdtsc();
        p_function();
        cycleSum += __rdtsc() - startCycles;
    }

    return cycleSum / p_iterations;
}

int main()
{
    const size_t iterations = 100000;

    while (true)
    {
        std::cout << "Stack():        \t" << Benchmark(Stack, iterations)           <<  "\tcycles\n";
        std::cout << "Pool():         \t" << Benchmark(Pool, iterations)            <<  "\tcycles\n";
        std::cout << "FastPool():     \t" << Benchmark(FastPool, iterations)        <<  "\tcycles\n";
        std::cout << "FastHeapPool(): \t" << Benchmark(FastHeapPool, iterations)    <<  "\tcycles\n";

        std::cin.get();

        system("CLS");
    }

    return 0;
}

4 теста:

  • Stack (классический способ)
  • Pool (Предварительное выделение пула байтов)в стеке
  • FastPool (предварительное выделение пула байтов в стеке без абстракции класса, без вызова метода)
  • FastHeapPool (предварительное выделение пула байтов в куче без абстракции класса,нет вызова метода)

Вот результат с MSVC v142 с использованием C ++ 17:

Отладка
Debug Benchmark

Выпуск
Release Benchmark

Ну ... Не то, что я ожидал!

  • FastPool выглядит как классический способ.Это означает, что 6 распределений не очень отличаются от 1 большого выделения.
  • Простой пул (с использованием класса BytePool) очень медленный, я думаю, это происходит из-за вызовов методов, которые, кажется, оптимизируются в режиме выпуска.
  • FastHeapPool - это катастрофа, даже в режиме выпуска распределение кучи и доступ к ней кажутся очень медленными (что я и ожидал)

Итак, мой вопрос:

Существует ли какой-либо подход, который может превзойти классический подход (6 распределений в стеке), и почему выделение в 6 раз больше значения типа int равно выделению один раз размером в 6 дюймов

Я говорю только о памяти, а не об оптимизации операций

Ответы [ 2 ]

4 голосов
/ 10 июля 2019

Ваш тест ужасно ошибочен. Методы Stack (), Pool () и FastPool () сводятся к NOP (они не DO ничего !!). new / delete, однако, имеют возможные побочные эффекты, поэтому приходится учитывать разницу в производительности релиза. Теперь пришло время узнать, что на самом деле делает распределение стека! Если в методе используется переменная, выделенная стеком, она, скорее всего, будет регистром (если только это не тип pod с побочными эффектами), и любую безумную концепцию, которую вы пытаетесь создать, чтобы имитировать это с памятью, просто будут порядки медленнее из-за задержек, пропадания кэша и т. д.

В прежние времена у нас было ключевое слово register для различения стека, выделенного var и регистра. Больше нет, потому что это было бессмысленно. В наши дни распределение стека происходит только тогда, когда у вас заканчиваются регистры, и вам необходимо поменять значение регистра в пространстве стека.

2 голосов
/ 10 июля 2019

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

В любом случае, у вас, похоже, неправильное представление о том, как работают компиляторы. Ни один из современных компиляторов не переводит программу построчно. Все они генерируют так называемое абстрактное синтаксическое дерево (AST) - представление того, что делает ваша программа. Затем это синтаксическое дерево сильно модифицируется, чтобы обеспечить вам наилучшую возможную оптимизацию производительности. (Циклы развертываются, значения предварительно рассчитываются, ...) Наконец, серверная часть компилятора создает исполняемый файл из синтаксического дерева, который оптимизирован для вашей системы. (Могут использоваться специальные инструкции для машины, если таковые имеются.)

Из-за всех этих этапов довольно сложно угадать, какой машинный код создается на вашем c ++. Во многих случаях компилятор даже генерирует один и тот же машинный код из совершенно разных подходов программирования. Поэтому в вашем примере невозможно сказать, какой код работает быстрее, не глядя на двоичный файл.

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

Если вы заинтересованы в компиляторах и оптимизации, вы должны проверить:

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...