Luchian đưa ra lời giải thích về lý do tại sao hành vi này xảy ra, nhưng tôi nghĩ rằng đó là một ý tưởng hay để chỉ ra một giải pháp khả thi cho vấn đề này và đồng thời hiển thị một chút về các thuật toán lãng quên bộ nhớ cache.
Thuật toán của bạn về cơ bản là:
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
A[j][i] = A[i][j];
Điều này thật kinh khủng đối với một CPU hiện đại. Một giải pháp là biết chi tiết về hệ thống bộ nhớ cache của bạn và điều chỉnh thuật toán để tránh những vấn đề đó. Hoạt động tuyệt vời miễn là bạn biết những chi tiết đó .. không đặc biệt là di động.
Chúng ta có thể làm tốt hơn thế không? Có, chúng tôi có thể: Một cách tiếp cận chung cho vấn đề này là các thuật toán lãng quên bộ đệm mà như tên gọi đã tránh được việc phụ thuộc vào kích thước bộ đệm cụ thể [1]
Giải pháp sẽ như thế này:
void recursiveTranspose(int i0, int i1, int j0, int j1) {
int di = i1 - i0, dj = j1 - j0;
const int LEAFSIZE = 32; // well ok caching still affects this one here
if (di >= dj && di > LEAFSIZE) {
int im = (i0 + i1) / 2;
recursiveTranspose(i0, im, j0, j1);
recursiveTranspose(im, i1, j0, j1);
} else if (dj > LEAFSIZE) {
int jm = (j0 + j1) / 2;
recursiveTranspose(i0, i1, j0, jm);
recursiveTranspose(i0, i1, jm, j1);
} else {
for (int i = i0; i < i1; i++ )
for (int j = j0; j < j1; j++ )
mat[j][i] = mat[i][j];
}
}
Hơi phức tạp hơn một chút, nhưng một thử nghiệm ngắn cho thấy một điều khá thú vị trên e8400 cổ của tôi với bản phát hành VS2010 x64, mã kiểm tra cho MATSIZE 8192
int main() {
LARGE_INTEGER start, end, freq;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&start);
recursiveTranspose(0, MATSIZE, 0, MATSIZE);
QueryPerformanceCounter(&end);
printf("recursive: %.2fms\n", (end.QuadPart - start.QuadPart) / (double(freq.QuadPart) / 1000));
QueryPerformanceCounter(&start);
transpose();
QueryPerformanceCounter(&end);
printf("iterative: %.2fms\n", (end.QuadPart - start.QuadPart) / (double(freq.QuadPart) / 1000));
return 0;
}
results:
recursive: 480.58ms
iterative: 3678.46ms
Chỉnh sửa: Về ảnh hưởng của kích thước: Nó ít rõ rệt hơn mặc dù vẫn đáng chú ý ở một mức độ nào đó, bởi vì chúng tôi đang sử dụng giải pháp lặp như một nút lá thay vì đệ quy xuống 1 (tối ưu hóa thông thường cho các thuật toán đệ quy). Nếu chúng tôi đặt LEAFSIZE = 1, bộ đệm không ảnh hưởng đến tôi [ 8193: 1214.06; 8192: 1171.62ms, 8191: 1351.07ms
- đó là bên trong lề lỗi, các dao động nằm trong khu vực 100ms; "điểm chuẩn" này không phải là điều mà tôi quá thoải mái nếu chúng tôi muốn các giá trị hoàn toàn chính xác])
[1] Nguồn cho nội dung này: Chà nếu bạn không thể nhận được một bài giảng từ một người làm việc với Leiserson và đồng ý về điều này .. Tôi cho rằng bài báo của họ là điểm khởi đầu tốt. Những thuật toán đó vẫn còn khá hiếm khi được mô tả - CLR có một chú thích duy nhất về chúng. Tuy nhiên, đó là một cách tuyệt vời để làm mọi người ngạc nhiên.
Chỉnh sửa (lưu ý: Tôi không phải là người đã đăng câu trả lời này; tôi chỉ muốn thêm câu này):
Đây là phiên bản C ++ hoàn chỉnh của đoạn mã trên:
template<class InIt, class OutIt>
void transpose(InIt const input, OutIt const output,
size_t const rows, size_t const columns,
size_t const r1 = 0, size_t const c1 = 0,
size_t r2 = ~(size_t) 0, size_t c2 = ~(size_t) 0,
size_t const leaf = 0x20)
{
if (!~c2) { c2 = columns - c1; }
if (!~r2) { r2 = rows - r1; }
size_t const di = r2 - r1, dj = c2 - c1;
if (di >= dj && di > leaf)
{
transpose(input, output, rows, columns, r1, c1, (r1 + r2) / 2, c2);
transpose(input, output, rows, columns, (r1 + r2) / 2, c1, r2, c2);
}
else if (dj > leaf)
{
transpose(input, output, rows, columns, r1, c1, r2, (c1 + c2) / 2);
transpose(input, output, rows, columns, r1, (c1 + c2) / 2, r2, c2);
}
else
{
for (ptrdiff_t i1 = (ptrdiff_t) r1, i2 = (ptrdiff_t) (i1 * columns);
i1 < (ptrdiff_t) r2; ++i1, i2 += (ptrdiff_t) columns)
{
for (ptrdiff_t j1 = (ptrdiff_t) c1, j2 = (ptrdiff_t) (j1 * rows);
j1 < (ptrdiff_t) c2; ++j1, j2 += (ptrdiff_t) rows)
{
output[j2 + i1] = input[i2 + j1];
}
}
}
}