Проблема
В течение долгого времени у меня создалось впечатление, что использование вложенного std::vector<std::vector...>
для имитации N-мерного массива вообще плохое, поскольку память не является гарантией непрерывности и может иметь недостатки в кэше. Я подумал, что лучше использовать плоский вектор и карту из нескольких измерений в 1D и наоборот. Итак, я решил проверить его (код указан в конце). Это довольно просто, я приурочил чтение/запись к вложенному 3D-вектору против моей собственной 3D-обертки 1D-вектора. Я скомпилировал код с g++
и clang++
, при включенной оптимизации -O3
. Для каждого запуска я изменил размеры, поэтому я могу получить довольно хорошее представление о поведении. К моему удивлению, это те результаты, которые я получил на своей машине MacBook Pro (Retina, 13 дюймов, конец 2012 года), 2,5 ГГц i5, 8 ГБ оперативной памяти, OS X 10.10.5:
g++ 5.2
dimensions nested flat
X Y Z (ms) (ms)
100 100 100 -> 16 24
150 150 150 -> 58 98
200 200 200 -> 136 308
250 250 250 -> 264 746
300 300 300 -> 440 1537
clang++ (LLVM 7.0.0)
dimensions nested flat
X Y Z (ms) (ms)
100 100 100 -> 16 18
150 150 150 -> 53 61
200 200 200 -> 135 137
250 250 250 -> 255 271
300 300 300 -> 423 477
Как вы можете видеть, обертка "сгладить" никогда не избивает вложенную версию. Более того, реализация g++ libstdС++ выполняется довольно плохо по сравнению с реализацией libС++, например, для 300 x 300 x 300
версия сглаживания почти в 4 раза медленнее, чем вложенная версия. libС++, похоже, имеет равную производительность.
Мои вопросы:
- Почему не сгладить версию быстрее? Разве это не должно быть? Я что-то пропустил в тестовом коде?
- Кроме того, почему g++ libstdС++ так плохо работает при использовании сглаженных векторов? Опять же, лучше не работать?
Код, который я использовал:
#include <chrono>
#include <cstddef>
#include <iostream>
#include <memory>
#include <random>
#include <vector>
// Thin wrapper around flatten vector
template<typename T>
class Array3D
{
std::size_t _X, _Y, _Z;
std::vector<T> _vec;
public:
Array3D(std::size_t X, std::size_t Y, std::size_t Z):
_X(X), _Y(Y), _Z(Z), _vec(_X * _Y * _Z) {}
T& operator()(std::size_t x, std::size_t y, std::size_t z)
{
return _vec[z * (_X * _Y) + y * _X + x];
}
const T& operator()(std::size_t x, std::size_t y, std::size_t z) const
{
return _vec[z * (_X * _Y) + y * _X + x];
}
};
int main(int argc, char** argv)
{
std::random_device rd{};
std::mt19937 rng{rd()};
std::uniform_real_distribution<double> urd(-1, 1);
const std::size_t X = std::stol(argv[1]);
const std::size_t Y = std::stol(argv[2]);
const std::size_t Z = std::stol(argv[3]);
// Standard library nested vector
std::vector<std::vector<std::vector<double>>>
vec3D(X, std::vector<std::vector<double>>(Y, std::vector<double>(Z)));
// 3D wrapper around a 1D flat vector
Array3D<double> vec1D(X, Y, Z);
// TIMING nested vectors
std::cout << "Timing nested vectors...\n";
auto start = std::chrono::steady_clock::now();
volatile double tmp1 = 0;
for (std::size_t x = 0 ; x < X; ++x)
{
for (std::size_t y = 0 ; y < Y; ++y)
{
for (std::size_t z = 0 ; z < Z; ++z)
{
vec3D[x][y][z] = urd(rng);
tmp1 += vec3D[x][y][z];
}
}
}
std::cout << "\tSum: " << tmp1 << std::endl; // we make sure the loops are not optimized out
auto end = std::chrono::steady_clock::now();
std::cout << "Took: ";
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << ms << " milliseconds\n";
// TIMING flatten vector
std::cout << "Timing flatten vector...\n";
start = std::chrono::steady_clock::now();
volatile double tmp2 = 0;
for (std::size_t x = 0 ; x < X; ++x)
{
for (std::size_t y = 0 ; y < Y; ++y)
{
for (std::size_t z = 0 ; z < Z; ++z)
{
vec1D(x, y, z) = urd(rng);
tmp2 += vec1D(x, y, z);
}
}
}
std::cout << "\tSum: " << tmp2 << std::endl; // we make sure the loops are not optimized out
end = std::chrono::steady_clock::now();
std::cout << "Took: ";
ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
std::cout << ms << " milliseconds\n";
}
ИЗМЕНИТЬ
Изменение Array3D<T>::operator()
возвращается к
return _vec[(x * _Y + y) * _Z + z];
в соответствии с предложением @1201ProgramAlarm действительно избавляется от "странного" поведения g++ в том смысле, что плоские и вложенные версии берут сейчас примерно одно и то же время, Однако это все еще интригует. Я думал, что вложенное будет намного хуже из-за проблем с кешем. Могу ли я просто повезти и все выделены в память?