Является ли хорошей идеей использовать vector <vector <double >> для формирования матричного класса для высокопроизводительного научного вычислительного кода?
37
Является ли хорошей идеей использовать vector<vector<double>>(используя std) для формирования матричного класса для высокопроизводительного научного вычислительного кода?
-1 Конечно, это плохая идея. Вы не сможете использовать blas, lapack или любую другую существующую матричную библиотеку с таким форматом хранения. Кроме того, вы вводите неэффективность данных нелокальности и косвенности данных
Томас Климпел
9
@ Томас Означает ли это, что стоит понизить голос?
AKID
33
Не отрицайте. Это законный вопрос, даже если это ошибочная идея.
Вольфганг Бангерт
3
std :: vector не является распределенным вектором, поэтому вы не сможете выполнять с ним много параллельных вычислений (за исключением машин с общей памятью), вместо этого используйте Petsc или Trilinos. Кроме того, каждый обычно имеет дело с разреженными матрицами, и вы будете хранить полные плотные матрицы. Для игры с разреженными матрицами вы можете использовать std :: vector <std :: map>, но опять же, это не очень хорошо работает, см. Пост @WolfgangBangerth ниже.
gnzlbg
3
попробуйте использовать std :: vector <std :: vector <double >> с MPI, и вам захочется снимать себя
pyCthon
Ответы:
43
Это плохая идея, потому что vector должен разместить столько объектов в пространстве, сколько строк в вашей матрице. Распределение стоит дорого, но в первую очередь это плохая идея, потому что данные вашей матрицы теперь существуют в ряде массивов, разбросанных по памяти, а не в одном месте, где кеш процессора может легко получить к нему доступ.
Это также расточительный формат хранения: std :: vector хранит два указателя, один на начало массива и один на конец, потому что длина массива гибкая. С другой стороны, чтобы это была правильная матрица, длины всех строк должны быть одинаковыми, и поэтому было бы достаточно сохранить количество столбцов только один раз, вместо того, чтобы каждая строка сохраняла свою длину независимо.
Это на самом деле хуже, чем вы говорите, потому что на std::vectorсамом деле хранит три указателя: начало, конец и конец выделенной области хранения (например, позволяет нам вызывать .capacity()). Эта емкость может отличаться от размера, что значительно ухудшает ситуацию!
user14717
18
В дополнение к причинам, указанным Вольфгангом, если вы используете a vector<vector<double> >, вам придется разыменовывать его дважды каждый раз, когда вы хотите извлечь элемент, что в вычислительном отношении обходится дороже, чем одна операция разыменования. Один из типичных подходов заключается в выделении одного массива (а vector<double>или а double *). Я также видел, как люди добавляли синтаксический сахар в классы матрицы, оборачивая вокруг этого единственного массива некоторые более интуитивные операции индексации, чтобы уменьшить количество «умственных издержек», необходимых для вызова правильных индексов.
@Wolfgang: в зависимости от размера плотной матрицы два дополнительных указателя на строку могут быть незначительными. Что касается разбросанных данных, можно подумать об использовании пользовательского распределителя, который гарантирует, что векторы находятся в непрерывной памяти. Пока память не используется повторно, даже стандартный распределитель будет использовать непрерывную память с разницей в два указателя.
@Geoff: если вы используете произвольный доступ и используете только один массив, вам все равно нужно рассчитать индекс. Не может быть быстрее.
На моей системе теперь явный победитель (компилятор gcc 4.7 с -O3)
время векторной матрицы печатает:
index 997:3
index 998:3
index 999:30xc7fc680xc7fc80
calc took:185.507 k=100000000
real 0m0.257s
user 0m0.244s
sys 0m0.008s
Мы также видим, что до тех пор, пока стандартный распределитель не перерабатывает освобожденную память, данные являются непрерывными. (Конечно, после некоторого освобождения это не гарантируется.)
матрица времени печатает:
index 997:1
index 998:1
index 999:10x7ff41f208f480x7ff41f208f50
calc took:187.349 k=100000000
real 0m0.257s
user 0m0.248s
sys 0m0.004s
Вы пишете: «В моей системе теперь явный победитель» - вы не имели в виду явного победителя?
AKID
9
-1 Понимание производительности hpc-кода может быть нетривиальным. В вашем случае размер матрицы просто превышает размер кэша, так что вы просто измеряете пропускную способность памяти вашей системы. Если я изменю N на 200 и увеличу количество итераций до 1000, я получу «calc take: 65» против «calc взял: 36». Если я дополнительно заменим a = a * a на a + = a1 * a2, чтобы сделать его более реалистичным, я получу «calc взял: 176« против »calc взял: 84». Таким образом, похоже, что вы можете потерять фактор два в производительности, используя вектор векторов вместо матрицы. Реальная жизнь будет более сложной, но это все еще плохая идея.
Томас Климпел
да, но попробуйте использовать std :: vectors с MPI, C выигрывает
PyCthon
4
Я не рекомендую это, но не из-за проблем с производительностью. Он будет немного менее производительным, чем традиционная матрица, которая обычно выделяется как большой кусок непрерывных данных, которые индексируются с использованием разыменования с одним указателем и целочисленной арифметики. Причиной снижения производительности являются главным образом различия в кешировании, но как только размер вашей матрицы станет достаточно большим, этот эффект будет амортизироваться, и если вы используете специальный распределитель для внутренних векторов, чтобы они были выровнены по границам кеша, то это еще больше смягчает проблему кеширования ,
Это само по себе не является достаточной причиной, чтобы не делать этого, по моему мнению. Причина для меня в том, что это создает много головной боли при кодировании. Вот список головных болей, это приведет к долгосрочному
Использование библиотек HPC
Если вы хотите использовать большинство библиотек HPC, вам нужно будет выполнить итерацию по вашему вектору и поместить все их данные в непрерывный буфер, потому что большинство библиотек HPC ожидают этого явного формата. Вспоминаются BLAS и LAPACK, но использовать универсальную библиотеку HPC MPI будет гораздо сложнее.
Больше возможностей для ошибки кодирования
std::vectorничего не знает о своих записях. Если вы заполняете a std::vectorбольшим количеством std::vectors, тогда ваша задача - убедиться, что все они имеют одинаковый размер, потому что помните, что мы хотим, чтобы матрица и матрицы не имели переменного числа строк (или столбцов). Таким образом, вам придется вызывать все правильные конструкторы для каждой записи вашего внешнего вектора, и любой, кто использует ваш код, должен сопротивляться искушению использовать std::vector<T>::push_back()любой из внутренних векторов, что приведет к поломке всего следующего кода. Конечно, вы можете запретить это, если вы пишете свой класс правильно, но гораздо проще реализовать это просто с большим непрерывным распределением.
HPC культура и ожидания
Программисты HPC просто ожидают данных низкого уровня. Если вы дадите им матрицу, существует ожидание, что если они захватят указатель на первый элемент матрицы и указатель на последний элемент матрицы, то все указатели между этими двумя являются действительными и указывают на элементы того же самого матрица. Это похоже на мой первый пункт, но отличается, потому что оно может быть связано не столько с библиотеками, сколько с членами команды или с кем-то, с кем вы делитесь своим кодом.
Проще рассуждать о производительности данных более низкого уровня
Переход к низкоуровневому представлению желаемой структуры данных облегчает вашу жизнь в долгосрочной перспективе для HPC. Использование таких инструментов, как perfи vtune, даст вам измерения счетчика производительности очень низкого уровня, которые вы попытаетесь объединить с традиционными результатами профилирования, чтобы повысить производительность вашего кода. Если в вашей структуре данных используется много модных контейнеров, будет трудно понять, что ошибки в кеше происходят из-за проблемы с контейнером или неэффективности самого алгоритма. Для более сложных контейнеров кода необходимы, но для матричной алгебры это на самом деле не так - вы можете обойтись просто 1std::vectorдля хранения данных, а не nstd::vectors, так что продолжайте.
Я также пишу тест. Для матрицы малого размера (<100 * 100) производительность аналогична для вектора <вектор <двойной >> и обернутого 1D вектора. Для матрицы большого размера (~ 1000 * 1000) лучше обернуть 1D вектор. Собственная матрица ведет себя хуже. Меня удивляет, что Эйген - худший.
Как уже отмечали другие, не пытайтесь делать с этим математику или делать что-либо действенное.
Тем не менее, я использовал эту структуру как временную, когда код должен собрать двумерный массив, размеры которого будут определены во время выполнения и после того, как вы начали хранить данные. Например, сбор векторных выходных данных из какого-то дорогостоящего процесса, в котором непросто точно рассчитать, сколько векторов нужно сохранить при запуске.
Вы можете просто объединить все ваши векторные входные данные в один буфер по мере их поступления, но код будет более долговечным и более читабельным, если вы используете a vector<vector<T>>.
Ответы:
Это плохая идея, потому что vector должен разместить столько объектов в пространстве, сколько строк в вашей матрице. Распределение стоит дорого, но в первую очередь это плохая идея, потому что данные вашей матрицы теперь существуют в ряде массивов, разбросанных по памяти, а не в одном месте, где кеш процессора может легко получить к нему доступ.
Это также расточительный формат хранения: std :: vector хранит два указателя, один на начало массива и один на конец, потому что длина массива гибкая. С другой стороны, чтобы это была правильная матрица, длины всех строк должны быть одинаковыми, и поэтому было бы достаточно сохранить количество столбцов только один раз, вместо того, чтобы каждая строка сохраняла свою длину независимо.
источник
std::vector
самом деле хранит три указателя: начало, конец и конец выделенной области хранения (например, позволяет нам вызывать.capacity()
). Эта емкость может отличаться от размера, что значительно ухудшает ситуацию!В дополнение к причинам, указанным Вольфгангом, если вы используете a
vector<vector<double> >
, вам придется разыменовывать его дважды каждый раз, когда вы хотите извлечь элемент, что в вычислительном отношении обходится дороже, чем одна операция разыменования. Один из типичных подходов заключается в выделении одного массива (аvector<double>
или аdouble *
). Я также видел, как люди добавляли синтаксический сахар в классы матрицы, оборачивая вокруг этого единственного массива некоторые более интуитивные операции индексации, чтобы уменьшить количество «умственных издержек», необходимых для вызова правильных индексов.источник
Нет, используйте одну из бесплатных библиотек линейной алгебры. Обсуждение различных библиотек можно найти здесь: Рекомендации по быстрой и полезной матричной библиотеке C ++?
источник
Это действительно такая плохая вещь?
@Wolfgang: в зависимости от размера плотной матрицы два дополнительных указателя на строку могут быть незначительными. Что касается разбросанных данных, можно подумать об использовании пользовательского распределителя, который гарантирует, что векторы находятся в непрерывной памяти. Пока память не используется повторно, даже стандартный распределитель будет использовать непрерывную память с разницей в два указателя.
@Geoff: если вы используете произвольный доступ и используете только один массив, вам все равно нужно рассчитать индекс. Не может быть быстрее.
Итак, давайте сделаем небольшой тест:
vectormatrix.cc:
А теперь используя один массив:
arraymatrix.cc
На моей системе теперь явный победитель (компилятор gcc 4.7 с -O3)
время векторной матрицы печатает:
Мы также видим, что до тех пор, пока стандартный распределитель не перерабатывает освобожденную память, данные являются непрерывными. (Конечно, после некоторого освобождения это не гарантируется.)
матрица времени печатает:
источник
Я не рекомендую это, но не из-за проблем с производительностью. Он будет немного менее производительным, чем традиционная матрица, которая обычно выделяется как большой кусок непрерывных данных, которые индексируются с использованием разыменования с одним указателем и целочисленной арифметики. Причиной снижения производительности являются главным образом различия в кешировании, но как только размер вашей матрицы станет достаточно большим, этот эффект будет амортизироваться, и если вы используете специальный распределитель для внутренних векторов, чтобы они были выровнены по границам кеша, то это еще больше смягчает проблему кеширования ,
Это само по себе не является достаточной причиной, чтобы не делать этого, по моему мнению. Причина для меня в том, что это создает много головной боли при кодировании. Вот список головных болей, это приведет к долгосрочному
Использование библиотек HPC
Если вы хотите использовать большинство библиотек HPC, вам нужно будет выполнить итерацию по вашему вектору и поместить все их данные в непрерывный буфер, потому что большинство библиотек HPC ожидают этого явного формата. Вспоминаются BLAS и LAPACK, но использовать универсальную библиотеку HPC MPI будет гораздо сложнее.
Больше возможностей для ошибки кодирования
std::vector
ничего не знает о своих записях. Если вы заполняете astd::vector
большим количествомstd::vector
s, тогда ваша задача - убедиться, что все они имеют одинаковый размер, потому что помните, что мы хотим, чтобы матрица и матрицы не имели переменного числа строк (или столбцов). Таким образом, вам придется вызывать все правильные конструкторы для каждой записи вашего внешнего вектора, и любой, кто использует ваш код, должен сопротивляться искушению использоватьstd::vector<T>::push_back()
любой из внутренних векторов, что приведет к поломке всего следующего кода. Конечно, вы можете запретить это, если вы пишете свой класс правильно, но гораздо проще реализовать это просто с большим непрерывным распределением.HPC культура и ожидания
Программисты HPC просто ожидают данных низкого уровня. Если вы дадите им матрицу, существует ожидание, что если они захватят указатель на первый элемент матрицы и указатель на последний элемент матрицы, то все указатели между этими двумя являются действительными и указывают на элементы того же самого матрица. Это похоже на мой первый пункт, но отличается, потому что оно может быть связано не столько с библиотеками, сколько с членами команды или с кем-то, с кем вы делитесь своим кодом.
Проще рассуждать о производительности данных более низкого уровня
Переход к низкоуровневому представлению желаемой структуры данных облегчает вашу жизнь в долгосрочной перспективе для HPC. Использование таких инструментов, как
perf
иvtune
, даст вам измерения счетчика производительности очень низкого уровня, которые вы попытаетесь объединить с традиционными результатами профилирования, чтобы повысить производительность вашего кода. Если в вашей структуре данных используется много модных контейнеров, будет трудно понять, что ошибки в кеше происходят из-за проблемы с контейнером или неэффективности самого алгоритма. Для более сложных контейнеров кода необходимы, но для матричной алгебры это на самом деле не так - вы можете обойтись просто1
std::vector
для хранения данных, а неn
std::vector
s, так что продолжайте.источник
Я также пишу тест. Для матрицы малого размера (<100 * 100) производительность аналогична для вектора <вектор <двойной >> и обернутого 1D вектора. Для матрицы большого размера (~ 1000 * 1000) лучше обернуть 1D вектор. Собственная матрица ведет себя хуже. Меня удивляет, что Эйген - худший.
источник
Как уже отмечали другие, не пытайтесь делать с этим математику или делать что-либо действенное.
Тем не менее, я использовал эту структуру как временную, когда код должен собрать двумерный массив, размеры которого будут определены во время выполнения и после того, как вы начали хранить данные. Например, сбор векторных выходных данных из какого-то дорогостоящего процесса, в котором непросто точно рассчитать, сколько векторов нужно сохранить при запуске.
Вы можете просто объединить все ваши векторные входные данные в один буфер по мере их поступления, но код будет более долговечным и более читабельным, если вы используете a
vector<vector<T>>
.источник