Класс матрицы C++ перегружен операторами, возвращающимися по ссылке


Я пишу шаблонный матричный класс, и я получаю переполнения стека при возврате по значению от операторов:+, -, * для больших матриц. Я бы предпочел как-то вернуться по ссылке, чтобы разгрузить стек и избежать дополнительного копирования, но тогда мне пришлось бы возвращать объект, построенный с new и нарушать общее правило "using delete for every new". Я не могу вернуться по значению из-за проблем копирования накладных расходов и ограничения стека, и я также не могу вернуться по значению ссылка из-за утечки памяти, так что же мне тогда делать?

Вот моя функция продукта (Матрица содержит 2D элементы массива):

    template<typename T, unsigned int n, unsigned int m> template<unsigned int m2>
Matrix<T,n,m2> Matrix<T,n,m>::operator*(Matrix<T,m,m2>& M) {
    T prod[n][m2];
    if(n*m < GPUAccelerationThreshold)
        for(int i = 0; i < n; i++)
            for(int j = 0; j < m2; j++) {
                prod[i][j] = elems[i][0] * M(0, j); 
                for(int p = 1; p < m; p++)
                    prod[i][j] += elems[i][p] * M(p, j); 
            }
    else {
        array_view<T, 2> product(n, m2, *prod);
        array_view<T, 2> a(n, m, *elems);
        array_view<T, 2> b(m, m2, M.elems[0]);

        parallel_for_each(
            product.extent, 
             [=](index<2> idx) restrict(amp) {
                int row = idx[0];
                int col = idx[1];
                for (int inner = 0; inner < m; inner++) {
                    product[idx] += a(row, inner) * b(inner, col);
                }
            }
        );
        product.synchronize();
    }


    return Matrix<T,n,m2>(prod);
}

Я пишу этот класс, потому что хочу увеличить некоторые матричные операции на GPU(с MS amp). Я искал существующее решение, нашел библиотеки ускоренной линейной алгебры GPU, но то, что я не мог найти в них, было простым матричным классом с+, -, * операторами. Может быть, кто-нибудь порекомендует мне что-нибудь?

3 2

3 ответа:

Три коротких замечания:

  • традиционно классы Matrix использовали динамические распределение. Вы не показываете свой класс Matrix, но если ваш данные есть:
    T myData[n][m];
    
    возможно, вы захотите изменить его на:
        std::vector myData;
    
    , инициализируя его до размера n * m в конструкторе, и вычисление единичного индекса в operator[] (который должен верните прокси, если вы хотите сделать любую проверку границ). Кроме того, вы можете использовать operator()( int i, int j ) для доступ к элементу: будь то myMatrix( i, j ) или myMatrix[i][j] предпочтительно для доступа зависит от кого ты спрашиваешь. Хотя это решение немного увеличивает общее использование памяти (но очень незначительно), это уменьшает размер стопки до пары дюжина байт, независимо от размера матрицы.
  • Кроме того, традиционно матричные классы не имели таких измерений, как часть их шаблонных аргументов. Является ли это хорошая вещь или нет-спорно. Вы получаете гораздо лучшую проверку типа (и ошибки во время компиляции, а не во время выполнения) с вашим решением, но если размеры являются аргументами к конструктор, скорее кроме аргументов шаблона, вы можете прочитать их из командной строки или конфигурационный файл, или еще что-нибудь. Это классическая безопасность компромисс между гибкостью и гибкостью. Что касается вашей проблемы, не имея таких размеров, как параметры шаблона означают, что все матрицы типа T имеют тот же тип. Таким образом, вы можете получить доступ к внутренним частям матрицы, которую вы возврат из функции-члена, и вам больше не нужно промежуточное звено T prod[n][m2]. Конечно, вы могли бы сделать все экземпляры Matrix друга, или просто использовать доступ функции для установки значений. Во всяком случае, вы не хотите intermediate T prod[n][m2]; это не только требует большого количества on стековая память, это означает, что вам придется скопировать результаты.
  • Наконец, и это несколько более продвинутый вариант: в лучшей матрице классы, operator* возвращает не матрицу, а помощник класс, по линии: шаблон класс MatrixMultiply { L const* myLhs; R const * миры; общественный: typedef t value_type; MatrixMultiply( L const& lhs, R const& rhs ) : myLhs( &lhs ) , миры (&rhs ) { } int getX () const { возврат myLhs - >getX(); } int getY () const { возвращение миры - >геты(); } T get (int i, int j) const { возвращение calculateIJ( myLhs, myRhs ); } }; Затем вы предоставляете шаблонный конструктор и назначение оператор который использует getX(), getY() и get( i, j ). Ваш operator* также является шаблоном, который возвращает MatrixMultiply: шаблон MatrixMultiply оператор*( л. константные& лхс, Р констит& РГО ) { возвращение MatrixMultiply( слева, справа ); } (Обратите внимание, что если L::value_type и R::value_type не являются идентично, это не будет компилироваться. Именно этого ты и хочешь, за исключением что сообщения об ошибках будут далеки от ясности.) В результате вы никогда не создадите промежуточное звено, временные матрицы. Как вы можете себе представить, вышеописанное решение значительно упрощается. Вам понадобится дополнительный код для обработки ошибок, а мне нет. думайте, что параллелизация тривиальна. Но он избегает построение всех промежуточных матриц, даже в сложных выражения. (Тот же метод может быть использован с использованием абстрактного базового класса, скажем MatrixAccessor, с чистыми виртуальными геттерами и выводом Matrix и все помощники, такие как MatrixMultiply из оно. ИМХО, это намного более читабельно, и сообщения об ошибках от компилятора, безусловно, будет более понятный. Результаты будут следующие: так же, как долго, как компилятор подставляет всех членов функции. Но это большое если , так как может быть существенная вложенность функций.)

Нет простого способа решить эту проблему. Вы не можете вернуть локальные переменные стека в качестве ссылки, потому что память "позади" переменной исчезнет, когда вы вернетесь. Поэтому у вас должно быть где-то специальное хранилище. Он не должен исходить из new / delete, но вам нужно иметь какое-то хранилище при создании копий данных.

Одно решение, конечно, будет иметь операцию с тремя операндами, поэтому вместо:

a = b + c;

Вы используете функцию:

Добавить(a, b, c);

Где A, b и c-ссылки.

Это делает код намного более запутанным, но я не могу придумать более очевидного способа решить проблему - если вы не хотите писать свою собственную функцию распределения/удаления (или сборщик мусора).

На самом деле я не могу полностью понять вашу идею... Бинарные операторы принимают два аргумента и создают результат. На самом деле вы можете видеть, что возвращаете новый созданный объект. Так что это единственный способ писать программы: выделять память, использовать ее, а затем удалять. На самом деле я даже не понимаю, что делает ваш конструктор. Если он просто копирует указатель на "prod", то результирующая матрица нарушается, когда вы возвращаете ее из функции, потому что память "prod" будет удалена, как только функция возвращает(потому что она создана в стеке). Так что вы не можете вернуть его по ссылке тоже.

Я вижу решение в том, чтобы выделить память в конструкторе матрицы. Если вы делаете его как шаблон в зависимости от размера матрицы, размер матрицы известен из параметров шаблона (я нахожу довольно странным делать шаблоны с размером матрицы в качестве аргумента.. Какой в этом смысл?). Таким образом, вы выделяете память в конструкторе с "new" и удаляете ее в деструкторе с "delete". Так эта память будет выделена в соответствии с методологией RAII, которая довольно хорошо работает в ООП. Затем вы реализуете такие методы, как setElement (i, j, value)и устанавливаете элементы в новой созданной матрице в своем двоичном операторе и возвращаете его.

Однако есть некоторые проблемы, о которых я хочу, чтобы вы позаботились. Конструктор копирования должен действительно скопировать матрицу, а не просто указатель (или несколько деструкторов попытаются уничтожить одну и ту же память), или вы можете запрограммировать модель "ленивого копирования", которая на самом деле копирует матрица изменений (см. wiki). Или вы можете сделать конструктор копирования приватным без реализации (чтобы предотвратить копирование вообще). Если вам не разрешено создавать такие методы, как "setElement", потому что вы не хотите, чтобы пользователь вашей библиотеки изменял значения матрицы, вы можете получить доступ к частным данным и методам (даже в объектах, которые мы получили в качестве аргумента или недавно создали) в таких операторах, потому что вы находитесь внутри метода класса.

Если вам нужно передать необработанный указатель на другое вычисление функция как это сделано в части "else", вы можете сделать конструктор из указателя, который будет копировать только указатель (но это опасный способ. Если вы передаете указатель, вы не должны обращаться к нему нигде, потому что класс matrix теперь босс) или копировать данные полностью(что медленнее, но вы можете передать туда указатели из стека или указатели, которые вам нужно взять под контроль после) в зависимости от вашего желания, и деструктор очистит его, когда матрица уничтожит. Или вы даже можете создать частный метод, такой как "getRawMatrix()" который вернет необработанный указатель на данные из Матрицы и передаст этот указатель вашим вычислительным функциям или даже просто получит указатель на необработанные данные, потому что вы находитесь внутри метода класса matrix.

Обычно вы выделяете память в конструкторе и делаете "ленивую копию" модели, потому что матрицы могут быть огромными. Внутри класса разрешен доступ к закрытым данным и членам, вот и создаются классы. Я надеюсь, что это будет полезно..