Удивительно неэффективный пользовательский распределитель для вектора & lt; char & gt;

Я хочу использовать вектор с пользовательским распределителем ниже, в котором construct() а также destroy() иметь пустое тело:

struct MyAllocator : public std::allocator<char> {
typedef allocator<char> Alloc;
//void destroy(Alloc::pointer p) {} // pre-c+11
//void construct(Alloc::pointer p, Alloc::const_reference val) {} // pre-c++11
template< class U > void destroy(U* p) {}
template< class U, class... Args > void construct(U* p, Args&&... args) {}
template<typename U> struct rebind {typedef MyAllocator other;};
};

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

std::vector<char, MyAllocator> v;
v.reserve(1000000); // or more. Make sure there is always enough allocated memory
while (true) {
v.resize(1000000);
// sleep for 10 ms
v.clear(); // or v.resize(0);
};

Я заметил, что при изменении размера потребление процессора возрастает с 30% до 80%, несмотря на то, что распределитель пуст construct() а также destroy() функции-члены. Из-за этого я ожидал бы очень минимального влияния или вообще никакого влияния (с включенной оптимизацией) на производительность. Как такое увеличение потребления возможно? Второй вопрос: почему при чтении памяти после любого изменения размера я вижу, что значение каждого символа в памяти с измененным размером равно 0 (я ожидал бы некоторые ненулевые значения, так как constuct() ничего не делает) ?

Моя среда g ++ 4.7.0, оптимизация уровня -O3 включена. ПК Intel двухъядерный, 4ГБ свободной памяти. Видимо звонки construct не могли быть оптимизированы вообще?

2

Решение

обновленный

Это полное переписывание. В исходном сообщении / моем ответе была ошибка, из-за которой я дважды тестировал один и тот же распределитель. К сожалению.

Ну, я вижу огромные различия в производительности. Я сделал следующий тестовый стенд, в котором предпринимаются некоторые меры предосторожности, чтобы гарантировать, что важные вещи не полностью оптимизированы. Затем я проверил (с -O0 -fno-inline), что распределитель construct а также destruct звонки получают ожидаемое количество раз (да):

#include <vector>
#include <cstdlib>

template<typename T>
struct MyAllocator : public std::allocator<T> {
typedef std::allocator<T> Alloc;
//void destroy(Alloc::pointer p) {} // pre-c+11
//void construct(Alloc::pointer p, Alloc::const_reference val) {} // pre-c++11
template< class U > void destroy(U* p) {}
template< class U, class... Args > void construct(U* p, Args&&... args) {}
template<typename U> struct rebind {typedef MyAllocator other;};
};

int main()
{
typedef char T;
#ifdef OWN_ALLOCATOR
std::vector<T, MyAllocator<T> > v;
#else
std::vector<T> v;
#endif
volatile unsigned long long x = 0;
v.reserve(1000000); // or more. Make sure there is always enough allocated memory
for(auto i=0ul; i< 1<<18; i++) {
v.resize(1000000);
x += v[rand()%v.size()];//._x;
v.clear(); // or v.resize(0);
};
}

Разница во времени отмечена:

g++ -g -O3 -std=c++0x -I ~/custom/boost/ test.cpp -o test

real    0m9.300s
user    0m9.289s
sys 0m0.000s

g++ -g -O3 -std=c++0x -DOWN_ALLOCATOR -I ~/custom/boost/ test.cpp -o test

real    0m0.004s
user    0m0.000s
sys 0m0.000s

Я могу только предположить, что то, что вы видите, связано со стандартной библиотекой, оптимизирующей операции распределителя для char (это тип POD).

Время становится еще дальше друг от друга, когда вы используете

struct NonTrivial
{
NonTrivial() { _x = 42; }
virtual ~NonTrivial() {}
char _x;
};

typedef NonTrivial T;

В этом случае распределитель по умолчанию занимает более 2 минут (все еще работает).
тогда как «фиктивный» MyAllocator тратит ~ 0,006 с. (Заметка что это вызывает неопределенное поведение, ссылающееся на элементы, которые не были должным образом инициализированы.)

2

Другие решения

(С исправлениями благодаря GManNickG и Джонатану Уэйкли ниже)

В C ++ 11 с постстандартной коррекцией, предложенной в http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3346.pdf, resize() создаст добавленные элементы, используя пользовательский распределитель.

В более ранних версиях resize() Значение инициализирует добавленные элементы, что требует времени.

Эти шаги инициализации не имеют ничего общего с распределением памяти, это то, что делается с памятью после ее выделения. Инициализация значения является неизбежным расходом.

Учитывая состояние соответствия стандартам C ++ 11 в современных компиляторах, стоило бы взглянуть на ваши заголовки, чтобы увидеть, какой подход используется.

Инициализация значения иногда была ненужной и неудобной, но также защищала многие программы от непреднамеренных ошибок. Например, кто-то может подумать, что он может изменить размер std::vector<std::string> чтобы иметь 100 «неинициализированных» строк, затем начать присваивать в них перед чтением из них, но обязательным условием для оператора присваивания является то, что изменяемый объект был правильно построен … в противном случае он, вероятно, найдет указатель мусора и попытается delete[] Это. Только тщательное размещение newКаждый элемент может безопасно построить их. Дизайн API ошибается на стороне надежности.

0

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector