(Да, я знаю, что есть question с почти одинаковым названием, но ответ был неудовлетворительным, см. ниже)
EDIT Извините, в исходном вопросе не использовалась оптимизация компилятора. Теперь это исправлено, но чтобы избежать тривиальной оптимизации и приблизиться к моему фактическому варианту использования, тест был разделен на два блока компиляции.
Тот факт, что конструктор std::vector<>
имеет линейную сложность, является досадой, когда дело доходит до критически важных приложений. Рассмотрим этот простой код
// compilation unit 1:
void set_v0(type*x, size_t n)
{
for(size_t i=0; i<n; ++i)
x[i] = simple_function(i);
}
// compilation unit 2:
std::vector<type> x(n); // default initialisation is wasteful
set_v0(x.data(),n); // over-writes initial values
когда значительное количество времени теряется путем построения x
. Общим способом, описанным в этом вопросе, является просто резервирование хранилища и использование push_back()
для заполнения данных:
// compilation unit 1:
void set_v1(std::vector<type>&x, size_t n)
{
x.reserve(n);
for(size_t i=0; i<n; ++i)
x.push_back(simple_function(i));
}
// compilation unit 2:
std::vector<type> x(); x.reserve(n); // no initialisation
set_v1(x,n); // using push_back()
Однако, как указано моим комментарием, push_back()
по сути медленный, делая этот второй подход на самом деле медленнее первого для достаточно простых конструктивных объектов, таких как size_t
s, когда для
simple_function = [](size_t i) { return i; };
Я получаю следующие тайминги (используя gcc 4.8 с -O3; clang 3.2 произвел ~ 10% медленный код)
timing vector::vector(n) + set_v0();
n=10000 time: 3.9e-05 sec
n=100000 time: 0.00037 sec
n=1000000 time: 0.003678 sec
n=10000000 time: 0.03565 sec
n=100000000 time: 0.373275 sec
timing vector::vector() + vector::reserve(n) + set_v1();
n=10000 time: 1.9e-05 sec
n=100000 time: 0.00018 sec
n=1000000 time: 0.00177 sec
n=10000000 time: 0.020829 sec
n=100000000 time: 0.435393 sec
Ускорение действительно возможно, если можно было бы исключить конструкцию по умолчанию, можно оценить по следующей версии обмана
// compilation unit 2
std::vector<type> x; x.reserve(n); // no initialisation
set_v0(x,n); // error: write beyond end of vector
// note: vector::size() == 0
когда мы получаем
timing vector::vector + vector::reserve(n) + set_v0(); (CHEATING)
n=10000 time: 8e-06 sec
n=100000 time: 7.2e-05 sec
n=1000000 time: 0.000776 sec
n=10000000 time: 0.01119 sec
n=100000000 time: 0.298024 sec
Итак, мой первый вопрос: Есть ли законный способ использования контейнера стандартной библиотеки, который даст эти последние тайминги? Или мне нужно прибегать к управлению памятью?
Теперь, что я действительно хочу, нужно использовать многопоточность для заполнения контейнера. Наивный код (с использованием openMP в этом примере для простоты, который на данный момент исключает clang)
// compilation unit 1
void set_v0(type*x, size_t n)
{
#pragma omp for // only difference to set_v0() from above
for(size_t i=0; i<n; ++i)
x[i] = simple_function(i);
}
// compilation unit 2:
std::vector<type> x(n); // default initialisation not mutli-threaded
#pragma omp parallel
set_v0(x,n); // over-writes initial values in parallel
теперь страдает тем, что инициализация по умолчанию всех элементов не является многопоточной, что приводит к потенциально серьезной деградации производительности. Ниже приведены тайминги для set_omp_v0()
и эквивалентного метода обмана (с использованием моего чипа macbook intel i7 с 4 ядрами, 8 гиперпотоков):
timing std::vector::vector(n) + omp parallel set_v0()
n=10000 time: 0.000389 sec
n=100000 time: 0.000226 sec
n=1000000 time: 0.001406 sec
n=10000000 time: 0.019833 sec
n=100000000 time: 0.35531 sec
timing vector::vector + vector::reserve(n) + omp parallel set_v0(); (CHEATING)
n=10000 time: 0.000222 sec
n=100000 time: 0.000243 sec
n=1000000 time: 0.000793 sec
n=10000000 time: 0.008952 sec
n=100000000 time: 0.089619 sec
Обратите внимание, что чит-версия в ~ 3,3 раза быстрее, чем серийная чит-версия, примерно так, как ожидалось, но стандартной версии нет.
Итак, мой второй вопрос: существует ли законный способ использования стандартного библиотечного контейнера, который даст эти последние тайминги в многопоточных ситуациях?
PS. Я нашел этот вопрос, где std::vector
обманут, чтобы избежать инициализации по умолчанию, предоставив ему uninitialized_allocator
,
Это уже не стандартное соответствие, но очень хорошо работает для моего тестового примера (см. Мой собственный ответ ниже и этот вопрос для деталей).