Сегодня я писал Пул Распределитель, когда у меня возник вопрос:
Возможно ли победить компилятор?
Под разгоном компилятора я имею в виду написание кодакоторый выполняет выделение памяти быстрее (меньше тактовых циклов), чем его самая простая версия (переменные выделения в стеке, одна за другой).
Итак, я придумал очень простой 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:
Отладка

Выпуск

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