Этот вопрос является продолжением этого: 2-мерный массив на куче, какая версия быстрее?
Я определил что-то вроде этого:
class Array
{
double *data;
int X;
int Y;
public:
Array(int X, int Y, double init = 0) : X(X), Y(Y)
{
data = new double [X*Y];
for (int i=0; i<X*Y; i++)
data[i] = init;
}
~Array() { delete[] data; }
double *operator[] (int x) { return (data+x*Y); }
};
Я хочу иметь преимущество скорости непрерывного массива с удобочитаемостью двумерного. Я думал, что class Array
сделает это, с
Array arr(1000,1000);
arr[x][y] = n;
(почти) так же быстро, как и обычная версия
double *arr = new double [1000*1000];
arr[x*1000+y] = n;
так как operator[]
определен в inline
.
Но простая версия выполняется намного быстрее, а инкапсулированный - немного быстрее, чем истинный двумерный double **arr;...; arr[x][y] = n;
double **arr;...; arr[x][y] = n;
не совсем верно, см. Edit2
Это нормально? Я собираюсь на VC++ 2010 с оптимизацией.
И, пожалуйста, никаких ответов с использованием vector
, я знаю об этой возможности, но меня интересуют более глубокие причины такого поведения...
Изменить:
Я прочитал комментарии, что мой class Array
делает 2 поиска, и я должен использовать прямой поиск 1 и ссылку возврата в double. Я пробовал это, и скорость не улучшается, это точно так же.
И я действительно не понимаю, почему мой класс выполняет 2 поиска:
Array arr(1000,1000);
arr[x][y] = n;
следует включить в:
(arr.data+x*arr.Y)[y] = n;
и далее:
*((arr.data+x*arr.Y)+y) = n;
что точно такое же:
arr.data[x*arr.Y+y] = n; // the proposed 1 lookup access
Я ошибаюсь?
Редактировать2:
Я снова приурочивался и заметил, что double **arr; arr[x][y] = n;
double **arr; arr[x][y] = n;
решение имеет разное время от 1:47 мин до 2:10 мин - в случайном стиле.
Все остальные решения:
class Array
как указано вышеdouble &operator() (int x, int y)
как предлагаетсяdouble *arr; arr[x*Y+y] = n;
double *arr; arr[x*Y+y] = n;
на самом деле такие же быстрые около 1:44 мин и всегда постоянны.
Вы не получаете выигрыш в производительности, потому что вам все равно придется делать два поиска в памяти в вашей обернутой версии. Арифметика int 1-й случай, доступ к элементу x * 1000 + y требует только одного поиска в памяти. Ваша версия с оболочкой возвращает указатель, который затем должен быть разыменован, что является медленной частью.
Попробуйте переустановить доступ к вашей оболочке в виде
inline double operator()(int x, int y) const {return data[x*Y + y];}
inline double& operator()(int x, int y) {return data[x*Y + y];}
и вызывая
arr(x,y) = n;
Я удивлен тем, что инкапсулированный один из них быстрее, чем простой 2-й массив, поскольку он может иметь только дополнительные накладные расходы.
EDIT: И теперь, когда я больше смотрю на проблему, я вижу, что ваше решение фактически не выполняет два поиска, так как ваш оператор overloaded [] ведет себя по-разному. См. Мой комментарий к исходному сообщению.
const
членом для решения этой проблемы .
Если я правильно понимаю, вы спрашиваете, почему ускорение использования 2D vs 1D кажется незначительным.
На мой взгляд, лучший способ сделать доступ к 2D-матрице - это использовать что-то вроде следующего.
double& operator()(const int row, const int col) inline{
return data[X*row + col];
}
double operator()(const int row, const int col) inline const{
return data[X*row + col];
}
Это дает вам ссылку и метод копирования.
Проблема со скоростью заключается в том, что она сильно зависит от базовой архитектуры вашего устройства.
Первая проблема - размер кеша. Очевидно, чем больше кеш, тем лучше и 1D-версия должна работать лучше, чем 2D в целом, как непрерывная память, лучше работает с кешем.
Также в вашем примере доступ к одному элементу в первый раз будет медленным, независимо от того, как упорядочена память, поскольку элемент не находится в кеше. Однако, если вы обращаетесь к этому элементу много раз или к элементу в той же области (кэш-строке), скорость должна быть более выраженной.
Второй вопрос - векторизация. В зависимости от операций, которые вы выполняете, особенно если они являются математическими операциями, такими как добавление и т.д., Они будут определять скорость. Если у вас есть более новый процессор с расширениями SSE или AVX, убедитесь, что компилятор компилирует эти функции, как правило, это делается автоматически при настройке. Вы можете убедиться, что, добавив -march = native и -msse3 или эквивалент Windows.
Еще одна небольшая оптимизация - сделать X, Y const. Это сделает вложение еще более эффективным, но, очевидно, с недостатком, который присваивает, становится болью.
Заключительное слово: профиль, посмотрите, где вы тратите больше всего времени и улучшаете его.
data[5]
в терминах указателей - это просто синтаксический сахар для data + sizeof(data[0])*5
Если вы расширите это с помощью [x][y]
, дополнительных обращений к памяти не будет, так как sizeof (data [0]) должно быть знать во время компиляции. Это точно такая же арифметика. Поэтому [x] [y] такой же, как я определил, однако компилятор генерирует за кулисами.
Другим преимуществом простого является то, что размеры являются константами, если вам не нужны размеры времени выполнения, попробуйте использовать шаблон:
Также, а не беспокоиться об управлении памятью - это будет лучше всего использовать с помощью std::unique_ptr
и вместо double[]
использовать std::array
template <class T, size_t X, size_t Y>
class Array
{
using custom_array=std::array<T,X*Y>;
std::unique_ptr<custom_array> data;
public:
Array() : data{new custom_array} {}
Array(const Array& rhs) : data{new custom_array(*(rhs.data))} {}
Array& operator=(const Array& rhs) {
if (&rhs != this)
{
*data=*(rhs.data);
}
return *this;
}
~Array() {}
T& operator() (int x, int y) { return data->at(x*Y+y); }
T operator() (int x, int y) const { return data->at(x*Y+y); }
};
Использование: Array<double,1000,1000> A; double b=A(3,4);
Array<double,1000,1000> A; double b=A(3,4);
Для яблок для яблок - выделяйте данные как std::array<double,X*Y>
, но поскольку вы хотите выделить кучу, используйте вышеописанное с помощью unique_ptr
.
Type data[N][M]
является синтаксическим сахаром, и разборка сгенерированных кодов операций покажет вам это. Вы правы, что смежный регион с одним поиском будет быстрее; ваш код просто этого не делает (пока). Я уверен, что некоторые ответы продемонстрируют это.