C++ 为什么转置512x512的矩阵比转置513x513的矩阵慢得多?

C++ 为什么转置512x512的矩阵比转置513x513的矩阵慢得多?,c++,performance,optimization,C++,Performance,Optimization,在对不同大小的方阵进行了一些实验之后,出现了一种模式。通常,转置大小为2^n的矩阵比转置大小为2^n+1的矩阵要慢。对于较小的n,差异不大 但是,如果值为512,则会出现较大差异。(至少对我来说) 免责声明:我知道由于元素的双重交换,该函数实际上并没有转置矩阵,但它没有任何区别 遵循代码: #define SAMPLES 1000 #define MATSIZE 512 #include <time.h> #include <iostream> int mat[MATS

在对不同大小的方阵进行了一些实验之后,出现了一种模式。通常,转置大小为
2^n
的矩阵比转置大小为
2^n+1的矩阵要慢。对于较小的
n
,差异不大

但是,如果值为512,则会出现较大差异。(至少对我来说)

免责声明:我知道由于元素的双重交换,该函数实际上并没有转置矩阵,但它没有任何区别

遵循代码:

#define SAMPLES 1000
#define MATSIZE 512

#include <time.h>
#include <iostream>
int mat[MATSIZE][MATSIZE];

void transpose()
{
   for ( int i = 0 ; i < MATSIZE ; i++ )
   for ( int j = 0 ; j < MATSIZE ; j++ )
   {
       int aux = mat[i][j];
       mat[i][j] = mat[j][i];
       mat[j][i] = aux;
   }
}

int main()
{
   //initialize matrix
   for ( int i = 0 ; i < MATSIZE ; i++ )
   for ( int j = 0 ; j < MATSIZE ; j++ )
       mat[i][j] = i+j;

   int t = clock();
   for ( int i = 0 ; i < SAMPLES ; i++ )
       transpose();
   int elapsed = clock() - t;

   std::cout << "Average for a matrix of " << MATSIZE << ": " << elapsed / SAMPLES;
}
#定义样本1000
#定义MATSIZE 512
#包括
#包括
int mat[MATSIZE][MATSIZE];
无效转置()
{
对于(int i=0;istd::cout这一解释来自于年的Agner Fog,它简化为如何访问数据并将其存储在缓存中

有关条款和详细信息,请参阅,我将在此缩小范围

缓存是以集合和行的形式组织的。一次只使用一个集合,其中包含的任何行都可以使用。一行可以镜像的内存乘以行数,就得到了缓存大小

对于特定的内存地址,我们可以使用以下公式计算哪个集合应该镜像它:

set = ( address / lineSize ) % numberOfsets
理想情况下,这类公式在集合之间提供了一个均匀的分布,因为每个内存地址都有可能被读取(理想情况下,我说过)

很明显,可能会发生重叠。如果缓存未命中,将在缓存中读取内存并替换旧值。请记住,每个集合都有若干行,其中最近使用最少的一行将被新读取的内存覆盖

我将尝试遵循Agner的示例:

假设每个集合有4行,每行包含64个字节。我们首先尝试读取集合
0x2710
,它位于集合
28
。然后我们还尝试读取地址
0x2F00
0x3700
0x3F00
0x4700
。所有这些都属于同一集合。在读取
0x4700
,集合中的所有行都将被占用。读取该内存将逐出集合中的现有行,即最初保存
0x2710
的行。问题在于,我们读取的地址(在本例中)是相隔
0x800
的。这是临界跨距(在本例中也是如此)

还可以计算临界跨步:

criticalStride = numberOfSets * lineSize
间隔为
criticalStride
的变量或多个间隔的变量争夺相同的缓存线

这是理论部分。接下来是解释(也是阿格纳,我将密切关注它以避免出错):

假设矩阵为64x64(请记住,效果因缓存而异),具有8kb缓存,每组4行*行大小为64字节。每行可以容纳矩阵中的8个元素(64位
int

关键跨距为2048字节,对应于矩阵的4行(在内存中是连续的)

假设我们正在处理第28行。我们正在尝试获取此行的元素,并将其与第28列中的元素交换。该行的前8个元素组成一条缓存线,但它们将进入第28列中的8条不同缓存线。请记住,关键跨距是相隔4行(一列中有4个连续元素)

当到达列中的元素16时(每组4行缓存线,相隔4行=故障),ex-0元素将从缓存中退出。当我们到达列的末尾时,所有以前的缓存线都将丢失,需要在访问下一个元素时重新加载(整行被覆盖)

如果大小不是临界跨距的倍数,则会将这一灾难的完美场景弄糟,因为我们不再处理垂直方向上临界跨距的元素,因此缓存重新加载的数量会大大减少


另一个免责声明——我只是想解释一下,希望我能理解,但我可能弄错了。无论如何,我在等待回复(或确认)from.:

Luchian解释了为什么会发生这种行为,但我认为最好是展示一种解决这个问题的可能方法,同时展示一些缓存无关算法

您的算法基本上可以:

for (int i = 0; i < N; i++) 
   for (int j = 0; j < N; j++) 
        A[j][i] = A[i][j];
编辑:关于大小的影响:虽然在某种程度上仍然很明显,但它不太明显,这是因为我们将迭代解决方案用作叶节点,而不是递归到1(递归算法的通常优化)。如果我们将LEAFSIZE设置为1,缓存对我没有影响[
8193:1214.06;8192:1171.62ms,8191:1351.07ms
-这在误差范围内,波动在100ms范围内;如果我们想要完全准确的值,这个“基准”不是我太喜欢的])

[1] 这方面的资料来源:如果你不能从与Leiserson和co合作的人那里得到一个关于这方面的讲座……我认为他们的论文是一个很好的起点。这些算法仍然很少被描述——CLR只有一个关于它们的脚注。这仍然是一个让人惊讶的好方法


编辑(注意:我不是发布此答案的人;我只是想添加此内容):
下面是一个完整的C++版本的代码:

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];
            }
        }
    }
}
模板
无效转置(初始常量输入,输出常量输出,
大小常量行、大小常量列、,
尺寸常数r1=0,尺寸常数
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
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];
            }
        }
    }
}