[C#] 2048×2048과 2047×2047의 배열 곱셈에서 성능이 크게 저하되는 이유는 무엇입니까?

MATLAB이 왜 행렬 곱셈에서 그렇게 빠른가?에서 언급 한 것처럼 행렬 곱셈 벤치마킹을하고 있습니다
.

이제 두 개의 2048×2048 행렬을 곱할 때 또 다른 문제가 있습니다 .C #과 다른 것 사이에는 큰 차이가 있습니다. 2047×2047 행렬 만 곱하려고하면 정상적인 것 같습니다. 비교를 위해 다른 것들도 추가했습니다.

1024×1024-10 초

1027×1027-10 초

2047×2047-90 초

2048×2048-300 초

2049×2049-91 초 (최신 정보)

2500×2500-166 초

2k x 2k의 경우 3 분 반 차이입니다.

2 차원 배열 사용

//Array init like this
int rozmer = 2048;
float[,] matice = new float[rozmer, rozmer];

//Main multiply code
for(int j = 0; j < rozmer; j++)
{
   for (int k = 0; k < rozmer; k++)
   {
     float temp = 0;
     for (int m = 0; m < rozmer; m++)
     {
       temp = temp + matice1[j,m] * matice2[m,k];
     }
     matice3[j, k] = temp;
   }
 }



답변

이것은 아마도 L2 캐시의 충돌과 관련이 있습니다.

matice1의 캐시 누락은 순차적으로 액세스되므로 문제가되지 않습니다. 그러나 matice2의 경우 전체 열이 L2에 맞는 경우 (예 : matice2 [0, 0], matice2 [1, 0], matice2 [2, 0] … 등에 액세스 할 때 아무런 문제가 없음) matice2로 캐시가 누락되었습니다.

변수의 바이트 주소가 X 인 경우 캐시가 작동하는 방식을 자세히 살펴 보려면 캐시 라인보다 (X >> 6) & (L-1)이됩니다. 여기서 L은 캐시의 총 캐시 라인 수입니다. L은 항상 2의 거듭 제곱입니다. 6은 2 ^ 6 == 64 바이트가 표준 캐시 크기입니다.

이것이 무엇을 의미합니까? 주소 X와 주소 Y를 가지고 있고 (X >> 6)-(Y >> 6)을 L로 나눌 수 있으면 (즉, 2의 큰 거듭 제곱) 동일한 캐시 라인에 저장됩니다.

이제 문제로 돌아가려면 2048과 2049의 차이점은 무엇입니까?

2048이 당신의 크기 일 때 :

& matice2 [x, k] 및 & matice2 [y, k]를 취하면 차이 (& matice2 [x, k] >> 6)-(& matice2 [y, k] >> 6)는 2048 * 4 (크기)로 나눌 수 있습니다. 플로트). 따라서 2의 큰 힘.

따라서 L2의 크기에 따라 많은 캐시 라인 충돌이 발생하고 L2의 작은 부분 만 사용하여 열을 저장하므로 실제로 전체 열을 캐시에 저장할 수 없으므로 성능이 저하됩니다. .

크기가 2049 인 경우 차이는 2049 * 4이며 2의 거듭 제곱이 아니므로 충돌이 줄어들고 열이 캐시에 안전하게 맞습니다.

이제이 이론을 테스트하기 위해 할 수있는 몇 가지가 있습니다.

이 matice2 [razmor, 4096]와 같이 배열 matice2 배열을 할당하고 razmor = 1024, 1025 또는 모든 크기로 실행하면 이전에 비해 성능이 매우 저하됩니다. 모든 열이 서로 충돌하도록 강제로 정렬하기 때문입니다.

그런 다음 matice2 [razmor, 4097]를 시도하고 어떤 크기로든 실행하면 훨씬 더 나은 성능을 볼 수 있습니다.


답변

캐싱 효과 일 것입니다. 2의 거듭 제곱 인 행렬 크기와 2의 거듭 제곱 인 캐시 크기를 사용하면 L1 캐시의 작은 부분 만 사용하여 결과를 크게 저하시킬 수 있습니다. 순진한 행렬 곱셈은 일반적으로 데이터를 캐시로 가져와야 할 필요성에 의해 제한됩니다. 타일링 (또는 캐시 불명확 한 알고리즘)을 사용하는 최적화 된 알고리즘은 L1 캐시를보다 잘 사용하는 데 중점을 둡니다.

다른 쌍 (2 ^ n-1,2 ^ n)의 시간을 정하면 비슷한 효과가 나타납니다.

matice2 [m, k]에 액세스하는 내부 루프에서 좀 더 자세히 설명하기 위해 matice2 [m, k]와 matice2 [m + 1, k]가 2048 * sizeof (float)만큼 서로 상쇄 될 수 있습니다. 따라서 L1 캐시에서 동일한 인덱스에 매핑됩니다. N-way 연관 캐시를 사용하면 일반적으로 이들 모두에 대해 1-8 개의 캐시 위치가 있습니다. 따라서 거의 모든 액세스가 L1 캐시 제거를 트리거하고 느린 캐시 또는 주 메모리에서 데이터를 가져옵니다.


답변

이것은 CPU 캐시 크기와 관련이있을 수 있습니다. 행렬 행렬의 2 행이 맞지 않으면 RAM에서 요소를 교체하는 시간이 느려집니다. 여분의 4095 요소는 열이 맞지 않도록하기에 충분할 수 있습니다.

귀하의 경우 2047 2d 매트릭스의 2 행은 16KB 메모리 (32 비트 유형으로 가정)에 속합니다. 예를 들어 LKB 캐시 (버스의 CPU에 가장 가까운)가 64KB 인 경우 한 번에 최소 4 개의 행 (2047 * 32)을 캐시에 넣을 수 있습니다. 더 긴 행으로 16KB 이상으로 행 쌍을 푸시하는 패딩이 필요한 경우 상황이 복잡해지기 시작합니다. 또한 캐시를 ‘누락’할 때마다 다른 캐시 또는 주 메모리에서 데이터를 교환하면 작업이 지연됩니다.

내 생각에 다른 크기의 행렬로 보는 런타임의 차이는 운영 체제가 사용 가능한 캐시를 얼마나 효과적으로 사용할 수 있는지에 영향을받습니다 (일부 조합은 문제가 있습니다). 물론 이것은 모두 내 단순화입니다.


답변

Louis Brandy는이 문제를 정확하게 분석하는 두 개의 블로그 게시물을 작성했습니다.

더 많은 캐시 크레이지계산 성능- 몇 가지 흥미로운 통계를 가진 초보자 사례 연구 와 동작을 더 자세히 설명하려는 시도는 실제로 캐시 크기 제한으로 이어집니다.


답변

시간이 더 큰 크기로 떨어지고 있다고 가정 할 때, 특히 문제가있는 매트릭스 크기에 대해 2의 거듭 제곱으로 인해 캐시 충돌이 발생할 가능성이 더 적습니까? 캐싱 문제에 대한 전문가는 아니지만 캐시 관련 성능 문제에 대한 훌륭한 정보는 여기에 있습니다 .


답변

matice2어레이를 세로 로 액세스 하면 캐시에서 더 많이 스왑 및 스왑됩니다. 배열을 대각선으로 미러링하여 [k,m]대신에 사용하여 액세스 할 수 있으면 [m,k]코드가 훨씬 빠르게 실행됩니다.

나는 이것을 1024×1024 행렬에 대해 테스트했으며 약 두 배 빠릅니다. 2048×2048 매트릭스의 경우 약 10 배 빠릅니다.


답변

캐시 앨리어싱

또는 캐시 탈곡 , 나는 용어를 화폐로 주조 할 수 있습니다.

캐시는 하위 비트로 인덱싱하고 상위 비트로 태그를 지정하여 작동합니다.

캐시에 4 워드가 있고 행렬이 4 x 4임을 이미징합니다. 열에 액세스하고 행의 길이가 2의 거듭 제곱이면 메모리의 각 열 요소가 동일한 캐시 요소에 매핑됩니다.

2의 거듭 제곱은 실제로이 문제에 최적입니다. 각각의 새 열 요소는 행별로 액세스하는 것처럼 다음 캐시 슬롯에 정확하게 매핑됩니다.

실제로, 태그는 연속적으로 증가하는 여러 개의 주소를 다루며,이 주소는 여러 인접 요소를 연속적으로 캐시합니다. 각각의 새 행이 매핑되는 버킷을 오프셋하면 열을 통과해도 이전 항목이 바뀌지 않습니다. 다음 열이 순회되면 전체 캐시가 다른 행으로 채워지고 캐시에 맞는 각 행 섹션이 여러 열에 도달합니다.

캐시는 DRAM보다 훨씬 빠르기 때문에 (주로 온칩이기 때문에) 적중률이 가장 중요합니다.