Теоретически, исходя из ваших ответов в комментариях, поскольку у вас доступен только один поток (т. Е. ЦП), все версии с нитями должны совпадать с однопоточными или более длинными из-за накладных расходов на управление потоками.,Вы не должны видеть никакого ускорения вообще, так как временной интервал, необходимый для решения одной части матрицы, является временным интервалом, который украден из другой параллельной задачи.С одним ЦП вы только разделяете время ресурсов ЦП - никакой реальной параллельной работы не происходит в данный отдельный отрезок времени.Я подозреваю, что причина, по которой ваша вторая реализация работает быстрее, состоит в том, что вы делаете меньше разыменования указателей и доступа к памяти во внутреннем цикле.Например, в основной операции C[i][j] += A[i][k] * B[k][j];
из multiply
и p_variantforloop_t
вы просматриваете множество операций на уровне сборки, многие из которых связаны с памятью.В «псевдокоде сборки» это будет выглядеть примерно так:
1) Переместить значение указателя из адреса, на который ссылается A
в стеке, в регистр R1
2) Увеличить адрес назарегистрировать R1
на значение вне стека, на которое ссылается переменная i
, j
или k
3) Переместить значение адреса указателя из адреса, на который указывает R1
, в R1
4) Увеличить адрес в R1
на значение вне стека, на которое ссылается переменная i
, j
или k
5) Переместить значение из адреса, на который указывает R1
в R1
(поэтому R1
теперь содержит значение A[i][k]
)
6) Выполните шаги 1-5 для адреса, на который ссылается B
в стеке, в регистр R2
(теперь R2
)содержит значение B[k][j]
)
7) Выполните шаги 1-4 для адреса, на который ссылается C
в стеке, в регистр R3
8) Переместите значение из адреса, на который указывает R3
в R4
(т. е. R4
содержит действительное значение C[i][j]
)
9) Умножить регистрR1
и R2
и сохранить в регистре R5
10) Добавить регистры R4
и R5
и сохранить в R4
11) Переместить окончательное значение из R4
обратно вадрес памяти, на который указывает R3
(теперь C[i][j]
имеет конечный результат)
И это при условии, что у нас есть 5 регистров общего назначения, и компилятор должным образом оптимизировал ваш C-код, чтобы использовать преимуществаиз них.Я оставил переменные индекса цикла i
, j
и k
в стеке, поэтому доступ к ним займет даже больше времени, чем если бы они были в регистрах ... это действительно зависит от того, сколько регистров должен иметь ваш компиляториграть на вашей платформе.Кроме того, если вы скомпилировали без какой-либо оптимизации, вы могли бы сделать намного больший доступ к памяти из стека, где некоторые из этих временных значений хранятся в стеке, а не в регистрах, а затем повторно обрабатываются из стека, что занимает намного больше времени.чем перемещение значений между регистрами.В любом случае, приведенный выше код оптимизировать намного сложнее.Это работает, но если вы работаете на 32-битной платформе x86, у вас не будет большого количества регистров общего назначения (хотя вы должны иметь как минимум 6).x86_64 имеет больше регистров для воспроизведения, но, тем не менее, есть все обращения к памяти, с которыми приходится бороться.
С другой стороны, такая операция, как tmp += a[k] * b[k]
из p_scalarproduct_t
в тесном внутреннем цикле, будетдвигаться НАМНОГО быстрее ... вот вышеописанная операция в псевдокоде сборки:
Был бы небольшой шаг инициализации для цикла
1) Сделать tmp
регистром R1
вместо переменной стека, и инициализируйте ее значение 0
2) Переместите значение адреса, на которое ссылается a
в стеке, в R2
3) Добавьте значение s
из стека в R2
и сохраните полученный адрес в R2
4) Переместите значение адреса, на которое ссылается b
в стеке, в R3
5) Добавьте значение s
из стека в R3
исохранить полученный адрес в R3
6) Установить счетчик в R6
, инициализированный в e - s
После одноразовой инициализации мы начнем фактический внутренний цикл
7) Переместить значение с адреса, на который указывает R2
, в R4
8) Переместите значение с адреса, на который указывает R3
, в R5
9) Умножьте R4
и R5
и сохраните результаты в R5
10) Добавьте R5
к R1
и сохраните результаты в R1
11) Приращение R2
и R3
12) Уменьшить счетчик в R6
, пока он не достигнет нуля, где мы завершаем цикл
Я не могу гарантировать, что именно так ваш компилятор настроит этот цикл, но вы можете видеть в целом на своем скалярном примере, что требуется меньше шагов во внутреннем цикле и, что более важно, меньше обращений к памяти. Поэтому больше можно сделать с операциями, которые используют исключительно регистры, а не с операциями, которые включают в себя области памяти и требуют выборки из памяти, которая намного медленнее, чем операции только с регистрами. Так что в целом он будет двигаться намного быстрее, и это не имеет ничего общего с потоками.
Наконец, я заметил, что у вас есть только два вложенных цикла для скалярного произведения, поэтому его сложность равна O (N ^ 2), где, как и для двух других ваших методов, у вас есть три вложенных цикла для сложности O (N ^ 3) , Это также будет иметь значение.