【问题标题】:Cache, loops and performance缓存、循环和性能
【发布时间】:2010-10-03 02:40:12
【问题描述】:

前段时间我写了一小段代码,在面试中询问,看看人们如何理解缓存和内存的概念:

#include "stdafx.h"
#include <stdlib.h>
#include <windows.h>
#include <iostream>

#define TOTAL 0x20000000

using namespace std;

__int64 count(int INNER, int OUTER)
{
    int a = 0;
    int* arr = (int*) HeapAlloc(GetProcessHeap(), 0, INNER * sizeof(int));
    if (!arr) {
        cerr << "HeapAlloc failed\n";
        return 1;
    }
    LARGE_INTEGER freq;
    LARGE_INTEGER startTime, endTime;
    __int64 elapsedTime, elapsedMilliseconds;
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&startTime);

    /* Начало работы */

    for (int i = 0; i < OUTER; i++) {
        for (int j = 0; j < INNER; j++) {
            a |= i;
            arr[j] = a;
        }
    }

    /* Конец работы */

    QueryPerformanceCounter(&endTime);
    elapsedTime = endTime.QuadPart - startTime.QuadPart;
    elapsedMilliseconds = (1000 * elapsedTime) / freq.QuadPart;
    HeapFree(GetProcessHeap(), 0, arr);
    return elapsedMilliseconds;
}

int _tmain(int argc, _TCHAR* argv[])
{
    __int64 time;
    for (int INNER = 0x10; INNER <= 0x2000000; INNER <<= 1) {
        int OUTER = TOTAL / INNER;
        time = count(INNER, OUTER);
        cout << INNER << "\t" << OUTER << "\t" << time << "\n";
    }
}

这就是它编译成的(循环本身):

00401062  xor         ecx,ecx 
00401064  test        ebp,ebp 
00401066  jle         count+83h (401083h) 
00401068  xor         eax,eax 
0040106A  test        ebx,ebx 
0040106C  jle         count+7Ch (40107Ch) 
0040106E  mov         edi,edi 
00401070  or          esi,ecx 
00401072  mov         dword ptr [edi+eax*4],esi 
00401075  add         eax,1 
00401078  cmp         eax,ebx 
0040107A  jl          count+70h (401070h) 
0040107C  add         ecx,1 
0040107F  cmp         ecx,ebp 
00401081  jl          count+68h (401068h) 

这是程序在我的机器上输出的内容:


LOG2(INNER) LOG2(OUTER)  Time, ms
4           25           523
5           24           569
6           23           441
7           22           400
8           21           367
9           20           358
10          19           349
11          18           364
12          17           378
13          16           384
14          15           357
15          14           377
16          13           379
17          12           390
18          11           386
19          10           419
20          9            995
21          8            1,015
22          7            1,038
23          6            1,071
24          5            1,082
25          4            1,119

我请人们解释发生了什么。随着内部数组的增长,周期数会随着时间的推移而减少。随着内部阵列的增长超出缓存,缓存未命中开始发生并且时间增加。到目前为止一切正常。

但是:当 INNER 数组大小为 16 时(这给了我们 64 字节的数据),尽管jmps 的数量更多,但性能提升很少在代码中。它很小(523 与 569),但可重现。

问题是:为什么会有这种提升?

【问题讨论】:

  • 那个输出表非常混乱 - 你能重新格式化它吗?
  • 我试着用 来做。它在预览中看起来不错,但在实际文章中很糟糕。
  • 尝试只使用空格来格式化它;这是一个等距字体,所以它应该可以工作。

标签: c++ performance memory caching


【解决方案1】:

可能是因为 64 是您机器上的高速缓存行大小,并且您基本上完全在单个高速缓存行之外运行每个迭代。

【讨论】:

  • @Quassnoi:HeapAlloc() 未在 64 字节边界上对齐内存。该数组更有可能跨越两个相邻的缓存行...
  • 为什么当大小小于 cahce 行大小时它运行得更慢?例如,当数组大小为 16 时,它不应该能够从 1 个单个高速缓存行处理 4 次外部循环的完整迭代,因此会更快吗?谢谢!
  • 它也可能更多是对分支预测的衡量。较小的内部循环会更频繁地失败。
【解决方案2】:

我不确定你在问什么。你只是在检查我们所知道的吗?何必?还是您问我们是因为您自己无法解释 64 字节的提升?或者是想知道这是否是一个好的面试问题,还是......?

无论如何,如果您的目的只是为您的面试问题提供背景,您应该删除所有不必要的代码。 HeapAlloc 重要吗?为什么不能在堆栈上声明数组?还是使用 new/malloc?

为什么需要在这个小测试程序中进行错误处理?同样,它只会分散注意力并增加更多噪音。 QPC 调用也是如此。我什至不会问你为什么需要一个预编译的头文件。

为了向受访者询问 6 行循环,他必须筛选出 16 行不相关的噪音。为什么?

正如评论中提到的,输出表基本上是不可读的。

我完全赞成看被采访者是否可以阅读代码,但我不认为提出有关性能和缓存特性的问题如此难以阅读的意义。

在任何情况下,一个 64 字节的 INNER 数组完全适合大多数 CPU 的缓存行。这意味着每次 OUTER 迭代都必须读取一个缓存行。

【讨论】:

  • 我在这里发布了整个代码,让您只需复制和编译它——以备不时之需。当然,在实际采访中,我只展示了循环。对不起,如果我伤害了你的感情:)
【解决方案3】:

澄清一下,他表中的一些空格实际上应该是 ,'s。大家可以想办法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-03-15
    • 1970-01-01
    • 1970-01-01
    • 2017-05-13
    • 2011-11-04
    • 2020-11-08
    • 2011-05-05
    • 2014-08-09
    相关资源
    最近更新 更多