0

我创建了一个大型布尔二维数组(5000X5000,总共 23MB 的 250 亿个元素)。然后我循环遍历并用随机真假实例化每个元素。然后我循环并阅读每一个元素。所有 2500 万个元素的读取时间约为 100 毫秒。

23MB 太大而无法放入 CPU 的缓存中,而且我认为我的程序太简单而无法从任何类型的编译器优化中受益,所以我是否可以得出结论,该程序正在大约 100 毫秒内从 RAM 读取 2500 万个元素?

    #include "stdafx.h"
    #include <iostream>
    #include <chrono>
    using namespace std;

    int _tmain(int argc, _TCHAR* argv[])
    {
        bool **locs;
        locs = new bool*[5000];
        for(int i = 0; i < 5000; i++)
            locs[i] = new bool[5000];
        for(int i = 0; i < 5000; i++)
            for(int i2 = 0; i2 < 5000; i2++)
                locs[i][i2] = rand() % 2 == 0 ? true : false;
        int *idx = new int [5000*5000];
        for(int i = 0; i < 5000*5000; i++)
            *(idx + i) = rand() % 4999;

        bool val;
        int memAccesses = 0;
        auto start = std::chrono::high_resolution_clock::now();
        for(int i = 0; i < 5000*5000; i++) {
            val = locs[*(idx + i)][*(idx + ++i)];
            memAccesses += 2;
        }
        auto finish = std::chrono::high_resolution_clock::now();

        std::cout << std::chrono::duration_cast<std::chrono::nanoseconds>(finish-start).count() << " ns\n";
        std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(finish-start).count() << " ms\n";
        cout << "TOTAL MEMORY ACCESSES: " << memAccesses << endl;
        cout << "The size of the array in memory is " << ((sizeof(bool)*5000*5000)/1048576) << "MB";

        int exit; cin >> exit;
        return 0;
    }

    /*
    OUTPUT IS:

        137013700 ns
        137 ms
        TOTAL MEMORY ACCESSES: 25000000
        The size of the array in memory is 23MB
    */
4

4 回答 4

4

正如其他答案所提到的,您看到的“速度”(即使 CPU 正在执行您的代码并且没有被编译器剥离)约为 250 MBps,这对于现代系统来说是非常非常低的数字。

但是,您的方法对我来说似乎有缺陷(诚然,我不是基准测试专家。)这是我看到的问题:

  1. 对于像这样的任何基准测试,即使是最简单的形式,您也需要区分random-accesssequence-access。内存不是一个随机存取设备(尽管它的名字)并且在这里表现很差。您的代码似乎在随机访问内存,因此您将其添加到您的结论中作为限定符:您“在大约 100 毫秒内从 RAM 的随机位置读取 2500 万个元素”。
  2. 这种基准测试的另一个方面是延迟与吞吐量的概念。同样,如果你想从你的数字和时间得出任何结论,你需要知道你在精确测量什么。
  3. 您计算的内存访问次数不正确。根据您的编译器生成的确切代码,这一行:

    val = locs[*(idx + i)][*(idx + ++i)];
    

    实际访问内存系统的次数可能在 4 到 9 次之间。

    • 充其量,如果、 和iidx在寄存器中或消除了对它们的访问,那么您需要读取、 读取(请记住,这是指向数组的指针数组,而不是2D 数组) read ,最后是 read 。其中一些可能会被缓存,但不太可能,因为正在进行缓存抖动。locsval*(idx + i)locs[*(idx + i)]locs*(idx + ++i)locs[*(idx + i)][*(idx + ++i)]
    • 在最坏的情况下,除了上述内容之外,您还需要两次访问++i(读取,然后写回) ,一次访问 ,idx一次访问,locs一次访问val。我不知道,您甚至可能需要i对两次出现的单次和/或两次读取进行另一次读取idx(由于指针别名等等。)
  4. 您需要注意,内存永远不会以单个字节甚至字的形式访问。内存总是以 cache-line 为单位进行读写。缓存线的大小可能因系统而异,尽管如今最常见的大小是 64 字节。因此,每次读取不在缓存中的内存位置时,都会从 RAM 中加载 64 字节(或更多)。如果您正在读取的内存位置位于缓存线边界(一个缓存线中的一些字节和下一个缓存线中的一些字节),那么您正在从 RAM 加载两个缓存线。给定一个健全的编译器和内存中正确对齐的变量,这不会经常发生,但它可能会发生。因此,您至少必须将计算出的带宽乘以缓存线的大小。
  5. 但是,如果您正在访问已经在缓存中的内存位置,那么您不会从 RAM 加载任何内容。你也需要在你的结论中考虑到这一点。
  6. 您还需要考虑缓存行逐出、缓存的关联性、级别数、一些缓存级别在指令和数据之间共享而一些不共享、一些在内核之间共享而一些不共享的事实,以及很多评估缓存和内存性能时的其他事情。
  7. DRAM 芯片也有很多奇怪和复杂的行为和特性。某些内存位置在其他位置之后读取速度更快(由于行和列的排列),由于刷新周期,某些访问可能会延迟很长时间(以 CPU 速度),其他设备可能正在使用 RAM 或总线那个RAM是打开的,等等等等。我对现代内存芯片的操作还很不熟悉,甚至我都知道那是一团糟。
  8. 必须考虑编译器优化对您的代码的影响。这意味着您必须在编译器完成后以汇编形式查看您的代码。您需要查看生成的程序集才能知道您的代码实际在做什么:是否以及哪些内存访问被优化了。

总而言之,我认为你不能从你的程序中得出很多有用的信息。很抱歉,但内存非常复杂!

于 2013-06-25T14:41:12.907 回答
2

不,读取并不总是一直到 RAM。执行读取(或写入)时,内存块会被拉入缓存。只要您正在读取的块已经在缓存中,就会使用缓存。如果您从不在缓存中的块请求数据,则访问 RAM 以获取内存块并将其放入缓存中。从缓存读取比从 RAM 读取要便宜得多。

再次编辑
,写入操作会导致内存中的块被拉入缓存。因为您在读取它们之前将值存储在程序中,所以您正在读取的数据很可能已经在您存储它时的缓存中。因此,读取值的循环很可能永远不需要访问 RAM。

于 2013-06-25T13:20:15.660 回答
2

缓存使用与程序的复杂性无关。每当从 RAM 中读取数据时,它都会进入缓存。由于缓存具有一定的大小,因此始终有可用的数据量。如果您访问前一个内存位置旁边的内存位置,它很可能已经被缓存。在这种情况下,不访问 RAM。

我建议阅读CPU 缓存维基百科条目以拓宽您的知识。

BTW:val = locs[*(idx + i)][*(idx + ++i)];你确定这是从左到右评估的吗?我不是。这是未定义的行为。我建议将++i以下内容放在访问器行下方。

//编辑:

从内存中读取的值没有做任何事情。这些指令很可能根本没有执行!检查字节码或添加一条(void) val;应该强制生成它的指令。

于 2013-06-25T13:23:45.513 回答
2

内存的部分()将一次存储在处理器缓存中,这允许处理器快速访问这些项目。但是,这种速度对于现代内存来说是完全合理的。即使是最慢的 DDR3 内存也能以大约 6 GB/s 的速度传输数据。

于 2013-06-25T13:24:34.520 回答