Почему производительность этих умножений матриц такая разная?

Я написал два класса матриц на Java, чтобы сравнить производительность их умножения матриц. Один класс (Mat1) хранит член double[][] A, где строка i матрицы равна A[i]. Другой класс (Mat2) хранит A и T, где T - это транспонирование A.

Допустим, у нас есть квадратная матрица M, и нам нужно произведение M.mult(M). Назовите продукт P.

Когда M является экземпляром Mat1, используемый алгоритм был простым:

P[i][j] += M.A[i][k] * M.A[k][j]
    for k in range(0, M.A.length)

В случае, когда M - это Mat2, я использовал:

P[i][j] += M.A[i][k] * M.T[j][k]

это тот же алгоритм, потому что T[j][k]==A[k][j]. На матрицах 1000x1000 второй алгоритм на моей машине занимает около 1,2 секунды, а первый - не менее 25 секунд. Я ожидал, что второй будет быстрее, но не настолько. Вопрос в том, почему это намного быстрее?

Мое единственное предположение состоит в том, что второй алгоритм лучше использует кеши ЦП, поскольку данные втягиваются в кеши кусками, превышающими 1 слово, и второй алгоритм извлекает выгоду из этого, проходя только строки, в то время как первый игнорирует данные, загруженные в кэширует, сразу переходя к строке ниже (которая составляет ~ 1000 слов в памяти, потому что массивы хранятся в основном порядке строк), ни одна из данных для которой не кэшируется.

Я спросил кого-то, и он подумал, что это из-за более дружелюбных шаблонов доступа к памяти (то есть, что вторая версия приведет к меньшему количеству программных ошибок TLB). Я вообще не думал об этом, но я могу видеть, как это приводит к меньшему количеству ошибок TLB.

Итак, что это такое? Или есть какая-то другая причина разницы в производительности?


person CromTheDestroyer    schedule 27.10.2010    source источник
comment
en.wikipedia.org/wiki/Locality_of_reference   -  person Mitch Wheat    schedule 27.10.2010
comment
Я думаю, что это предложение по обмену стеками может представлять интерес для ты. Если это так, покажите свою поддержку и помогите перейти в бета-версию.   -  person greatwolf    schedule 17.01.2011


Ответы (4)


Это из-за локальности ваших данных.

В ОЗУ матрица, хотя и двумерная, с вашей точки зрения, конечно же, хранится как непрерывный массив байтов. Единственное отличие от одномерного массива состоит в том, что смещение вычисляется путем интерполяции обоих индексов, которые вы используете.

Это означает, что если вы обращаетесь к элементу в позиции x,y, он вычислит x*row_length + y, и это будет смещение, используемое для ссылки на элемент в указанной позиции.

Что происходит, так это то, что большая матрица хранится не только на странице памяти (именно так ваша ОС управляет оперативной памятью, разбивая ее на фрагменты), поэтому она должна загрузить в кеш ЦП правильную страницу, если вы попытаетесь получить доступ к элемент, которого еще нет.

Пока вы непрерывно выполняете умножение, вы не создаете никаких проблем, поскольку вы в основном используете все коэффициенты страницы, а затем переключаетесь на следующий, но если вы инвертируете индексы, происходит то, что каждый отдельный элемент может содержаться в разные страницы памяти, поэтому каждый раз, когда ему нужно запрашивать в ОЗУ другую страницу, это почти для каждого отдельного умножения, которое вы делаете, поэтому разница такая аккуратная.

(Я упростил все объяснение, просто чтобы дать вам общее представление об этой проблеме)

В любом случае я не думаю, что это вызвано самой JVM. Возможно, это связано с тем, как ваша ОС управляет памятью процесса Java.

person Jack    schedule 27.10.2010
comment
В ОЗУ матрица, хотя и двумерная, с вашей точки зрения, конечно же, хранится как непрерывный массив байтов.. Это НЕ ИСТИНА для Java. В Java двумерный массив представлен как массив массивов. Расположение массивов на каждом уровне зависит от 1) того, как они были размещены, и 2) от того, сохранил ли сборщик мусора их вместе. - person Stephen C; 27.10.2010
comment
Стивен К .: Это правда, но мои массивы были размещены как: int n; новый двойной [n] [n]; поэтому очевидно, что jvm попытается выделить его в один непрерывный кусок - person CromTheDestroyer; 27.10.2010
comment
JIT вмешается, все будет оптимизировано, особенно если это примитивный тип данных ... не думайте, что JVM не заботится о том, что вы работаете с матрицей чисел, иначе Java никогда не сможет получить производительность, поэтому рядом с C / C ++. Без использования нативного типа производительность в обоих случаях была бы плоха :) - person Jack; 27.10.2010
comment
@Jack - у тебя есть какие-нибудь ссылки на это? Или это всего лишь гипотеза? - person Stephen C; 27.10.2010
comment
@CromTheDestroyer - очевидно! = Факт :-) - person Stephen C; 27.10.2010
comment
Я ссылаюсь на то, что мне пришлось написать структуру анализа данных, которая интенсивно использовала матрицы. Так что я действительно мог оценить разницу между использованием примитивных типов и составных типов (особенно при работе с большими матрицами ~ 100 тыс. Строк). В любом случае должно быть легко увидеть, есть ли в спецификации JVM инструкции для матриц. Дай мне проверить.. - person Jack; 27.10.2010
comment
В любом случае я не буду беспокоиться о том, является ли это непрерывным фрагментом или нет, поскольку также предполагается, что он хранится в виде массива массивов, если вы загружаете один из массивов (давайте подумаем о строке) и используете его только для 1 значения, проблема остается точно такой же .. (кстати, интуиция была приятным выражением, даже если мне нужен был словарь :) - person Jack; 27.10.2010
comment
Мне не удалось найти в Интернете что-то достаточно новое, чтобы считаться заслуживающим доверия, поэтому до тех пор, пока не будет доказан какой-либо факт, мы можем предположить, что это реализовано в виде массива массивов, как заявил Стивен С. На самом деле это точно так, как это было реализовано в более старых версиях Java (по крайней мере, 1.4.2), но, к сожалению, когда речь идет об определенных характеристиках, JIT действительно ведет себя как черный ящик :( - person Jack; 27.10.2010
comment
@Jack - я не это имел в виду под ссылкой. Я имел в виду ссылку на веб-страницу (желательно от Sun / Oracle) или ссылку на опубликованный документ, в котором описывается, как HotSpot реализует многомерные массивы. - person Stephen C; 27.10.2010
comment
@Jack - До некоторой степени JVM должна реализовать double [] [] как массив объектов массива. JLS требует, чтобы ((double[][])obj)[1] оценивал ссылку, неотличимую от нормальной double[] ссылки. Теоретически JIT может провести глобальный анализ, чтобы определить, что внутренние ссылки на массив не нужны, и можно использовать непрерывный блок памяти. Однако последующая динамическая загрузка может сделать этот анализ недействительным, в результате чего JVM окажется в невозможной ситуации, когда потребуется найти и преобразовать представления существующих многомерных массивов объектов. - person Stephen C; 27.10.2010
comment
Связано с SO stackoverflow.com/ questions / 2512082 / и stackoverflow.com/questions/2368761/ - person andersoj; 27.10.2010

Гипотезы кеширования и TLB разумны, но я хотел бы увидеть полный код вашего теста ... а не только фрагменты псевдокода.

Другая возможность заключается в том, что разница в производительности связана с тем, что ваше приложение использует на 50% больше памяти для массивов данных в версии с транспонированием. Если размер кучи вашей JVM невелик, возможно, из-за этого сборщик мусора запускается слишком часто. Это вполне могло быть результатом использования размера кучи по умолчанию. (Три лота по 1000 x 1000 x 8 байту ~ 24Мб)

Попробуйте установить начальный и максимальный размеры кучи, чтобы (скажем) удвоить текущий максимальный размер. Если это не имеет значения, то это не просто проблема с размером кучи.

person Stephen C    schedule 27.10.2010
comment
Возможно, произошло недоразумение, но корпус, в котором хранится больше данных, быстрее. И до тех пор, пока умножение не закончится, сборщик мусора не так уж и много, так что это не могло повлиять на время. - person CromTheDestroyer; 27.10.2010

Легко догадаться, что проблема может заключаться в местности, а может быть, и так, но это все еще предположение.

Гадать не надо. Ответ на этот вопрос могут дать два метода - пошаговое выполнение и случайная пауза.

Если вы пошагово выполните медленный код, вы можете обнаружить, что он делает много вещей, о которых вы даже не мечтали. Например, спросите вы? Попробуйте и узнайте. То, что вы должны увидеть на уровне машинного языка, - это эффективное пошаговое выполнение внутреннего цикла без лишних движений.

Если он действительно проходит через внутренний цикл без лишних движений, то случайная пауза предоставит вам информацию. Поскольку медленный занимает в 20 раз больше времени, чем быстрый, это означает, что 95% времени он делает то, что ему не нужно. Так что посмотрим, что это такое. Каждый раз, когда вы приостанавливаете его, вероятность 95%, что вы увидите, что это такое и почему.

Если в медленном случае выполняемые инструкции кажутся такими же эффективными, как и в быстром случае, то местоположение кеша является разумным предположением о том, почему он медленный. Я уверен, что после того, как вы устраните любую другую глупость, которая может происходить, эта локализация кэша будет доминировать.

person Mike Dunlavey    schedule 27.10.2010

Вы можете попробовать сравнить производительность между JDK6 и OpenJDK7, учитывая это набор результатов ...

person andersoj    schedule 27.10.2010