행렬 전치 및 열 주요 메모리 레이아웃 유지
문제 설명 : 행렬의 행 노름
랜덤 행렬 M의 모든 열의 규범을 계산하는이 장난감 예제를 고려하십시오.
julia> M = rand(Float64, 10000, 10000);
julia> @time map(x -> norm(x), M[:,j] for j in 1:size(M)[2]);
0.363795 seconds (166.70 k allocations: 770.086 MiB, 27.78% gc time)
그런 다음 행 표준을 계산합니다.
julia> @time map(x -> norm(x), M[:,i] for i in 1:size(M)[1]);
1.288872 seconds (176.19 k allocations: 770.232 MiB, 0.37% gc time)
두 실행 사이의 요소는 매트릭스의 메모리 레이아웃 (열 주요) 때문이라고 생각합니다. 실제로 행 규범의 계산은 연속적이지 않은 데이터에 대한 루프이므로 캐시 미스가있는 벡터화되지 않은 코드가 생성됩니다. 두 표준 계산에 대해 동일한 실행 시간을 갖고 싶습니다.
M
행의 규범을 계산할 때 동일한 속도를 얻기 위해의 레이아웃 을 행 전공 으로 변환 할 수 있습니까?
나는 무엇을 시도 했는가
나는 함께 노력 transpose
하고 permutedims
는 이러한 기능을 사용하는 경우 메모리가 행 주 (원래 행렬의 주요 그래서 열)에 지금 것 같다, 성공하지.
julia> Mt = copy(transpose(M));
julia> @time map(x -> norm(x), Mt[j,:] for j in 1:size(M)[2]);
1.581778 seconds (176.19 k allocations: 770.230 MiB)
julia> Mt = copy(permutedims(M,[2,1]));
julia> @time map(x -> norm(x), Mt[j,:] for j in 1:size(M)[2]);
1.454153 seconds (176.19 k allocations: 770.236 MiB, 9.98% gc time)
내가 사용하는 copy
새로운 레이아웃을 강제하려고 여기.
전치의 열 중심 레이아웃 또는 원래 행렬의 행 중심 레이아웃을 어떻게 강제 할 수 있습니까?
편집하다
@mcabbott와 @ przemyslaw-szufel이 지적했듯이 내가 보여준 마지막 코드에 오류가 있었는데 Mt
, 열의 표준 대신 행의 표준을 계산했습니다 .
Mt
대신 줄 의 규범에 대한 테스트 :
julia> Mt = transpose(M);
julia> @time map(x -> norm(x), M[:,j] for j in 1:size(M)[2]);
1.307777 seconds (204.52 k allocations: 772.032 MiB, 0.45% gc time)
julia> Mt = permutedims(M)
julia> @time map(x -> norm(x), M[:,j] for j in 1:size(M)[2]);
0.334047 seconds (166.53 k allocations: 770.079 MiB, 1.42% gc time)
그래서 결국에는 permutedims
예상대로 칼럼 메이저 매장 인 것 같습니다 . 실제로 Julia 배열은 항상 column-major에 저장됩니다. 열 중심 저장된 행렬 transpose
의 행 중심이기 때문에 일종의 예외 view
입니다.
답변
여기에는 몇 가지 문제가 있습니다.
- 코드를 잘못 벤치마킹하고 있습니다. 대부분의 경우 첫 번째 실행에서 컴파일 된 코드를 테스트하고 두 번째 실행에서 컴파일되지 않은 코드 (따라서 컴파일 시간 측정)를 테스트합니다. 항상
@time
두 번 실행 하거나 대신 BenchmarkTools를 사용해야합니다. - 코드가 비효율적입니다-불필요한 메모리 복사
- M의 유형은 불안정하므로 측정에는 일반적으로 Julia 기능을 실행하는 경우가 아닌 유형을 찾는 시간이 포함됩니다.
- 람다가 필요하지 않습니다. 함수를 직접 구문 분석 할 수 있습니다.
- @mcabbott에서 언급했듯이 코드에 버그가 포함되어 있으며 동일한 것을 두 번 측정하고 있습니다. 코드를 정리하면 다음과 같습니다.
julia> using LinearAlgebra, BenchmarkTools
julia> const M = rand(10000, 10000);
julia> @btime map(norm, @view M[:,j] for j in 1:size(M)[2]);
49.378 ms (2 allocations: 78.20 KiB)
julia> @btime map(norm, @view M[i, :] for i in 1:size(M)[1]);
1.013 s (2 allocations: 78.20 KiB)
이제 데이터 레이아웃에 대한 질문입니다. Julia는 열 중심 메모리 레이아웃을 사용하고 있습니다. 따라서 열에서 작동하는 작업은 행에서 작동하는 작업보다 빠릅니다. 한 가지 가능한 해결 방법은 다음의 전치 사본을 갖는 것입니다 M
.
const Mᵀ = collect(M')
복사하는 데 약간의 시간이 필요하지만 나중에 성능을 일치시킬 수 있습니다.
julia> @btime map(norm, @view Mᵀ[:,j] for j in 1:size(M)[2]);
48.455 ms (2 allocations: 78.20 KiB)
julia> map(norm, Mᵀ[:,j] for j in 1:size(M)[2]) == map(norm, M[i,:] for i in 1:size(M)[1])
true
규범을 계산할 때 각 열 / 행의 복사본을 만드는 데 많은 시간을 낭비하고 있습니다. view
대신 s를 사용 하거나 더 나은 방법으로 eachcol
/를 사용하십시오 eachrow
.
julia> M = rand(1000, 1000);
julia> @btime map(norm, $M[:,j] for j in 1:size($M, 2)); # slow and ugly
946.301 μs (1001 allocations: 7.76 MiB)
julia> @btime map(norm, eachcol($M)); # fast and nice 223.199 μs (1 allocation: 7.94 KiB) julia> @btime norm.(eachcol($M)); # even nicer, but allocates more for some reason.
227.701 μs (3 allocations: 47.08 KiB)