多线程应用程序带宽的测量

8
什么是测量我的应用程序(多线程,使用OpenMP编写)使用的带宽最简单和最有效的方法?我运行STREAM以获取最大可持续带宽,现在想知道是否饱和了整个可用带宽。

我找到了一些相关问题(例如主内存带宽测量),但我找不到这个问题的答案;

遗憾的是,我不能使用VTune,但我可以使用PAPI计数器;

我的主要目标是找出我的应用程序的可扩展性差是否与内存带宽的饱和有关。

谢谢

2个回答

4

有许多方法可以(从命令行)获取整个应用程序的带宽,但听起来您想单独查看一些内核。在这种情况下,使用PAPI调用包装代码的部分是完全合理的方法。

您可以在系统上使用PAPI事件计数器(papi_avail)来找到总的加载/存储指令数量,并且如果你知道你的加载/存储大小,就可以得到内存带宽。或者,您可以计算缓存中的命中次数,并乘以线路大小来推断系统中实际传输的数据量。PAPI wiki的各个地方都有文档,例如这里提供高级接口,这里提供一些有用的派生公式。

这里有一个编码简单示例,以明智的方式和不友好的转置方式进行矩阵-向量乘法。请注意,调用PAPI_read_counters会重置计数器,这正是我们想要的。

#include <stdio.h>
#include <stdlib.h>
typedef char * caddr_t;
#include <papi.h>
#include <sys/time.h>

int init(float ***a, float **x, float **y, int size);
void report_results(char *tname, long_long *values, const int n, double wtime);
void sensible_matvec(float **a, float *x, float *y, int size);
void wrong_order_matvec(float **a, float *x, float *y, int size);
void tick(struct timeval *t);
double tock(struct timeval *t);

#define NUM_EVENTS 3
int main(int argc, char **argv) {
    const int matsize = 4096;

    float **a, *x, *y;
    init(&a, &x, &y, matsize);

    int events[NUM_EVENTS] = {PAPI_L1_DCM, PAPI_LST_INS, PAPI_FP_INS};
    long_long values[NUM_EVENTS];

    double walltime;
    struct timeval t;

    if (PAPI_start_counters(events, NUM_EVENTS) != PAPI_OK) {
       fprintf(stderr, "Error starting PAPI counters; aborting\n");
       exit(1);
    }

    tick(&t);
    sensible_matvec(a, x, y, matsize);
    PAPI_read_counters(values, NUM_EVENTS);
    walltime = tock(&t);

    report_results("Sensible", values, NUM_EVENTS, walltime);

    tick(&t);
    wrong_order_matvec(a, x, y, matsize);
    PAPI_stop_counters(values, NUM_EVENTS);
    walltime = tock(&t);

    report_results("Wrong order", values, NUM_EVENTS, walltime);

    return 0;
}

void report_results(char *tname, long_long *values, const int n, double wtime) {
    long_long total_mem = values[1];
    long_long total_flops = values[2];
    long_long l1misses = values[0];
    printf("Test %s: time elapsed = %f, memory accesses = %lld, flop = %lld\n",
            tname, wtime, total_mem, total_flops);
    printf("\tMemory bandwidth (MB/sec) = %f\n", 1.0*total_mem*sizeof(float)/(wtime*1024*1024));
    printf("\tL1 cache miss rate = %f\n", 1.0*l1misses/total_mem);
    printf("\tMFLOPS = %lf\n\n", 1.0*total_flops/(wtime*1024*1024));
}

int alloc2d(float ***a, int n);
int free2d(float ***a, int n);
int alloc1d(float **x, int n);
int free1d(float **x, int n);

int init(float ***a, float **x, float **y, int size) {
    if (alloc2d(a,size))
        return -2;

    if (alloc1d(x,size)) {
        free2d(a,size);
        return -2;
    }

    if (alloc1d(y,size)) {
        free2d(a,size);
        free1d(x,size);
        return -3;
    }

    for (int i=0; i<size; i++) {
            (*x)[i] = (float)i;
            (*y)[i] = 0.;
    }

    for (int i=0; i<size; i++) {
        for (int j=0; j<size; j++) {
            (*a)[i][j] = i;
        }
    }

    return 0;
}
void sensible_matvec(float **a, float *x, float *y, int size) {
    for (int i=0; i<size; i++) {
        for (int j=0; j<size; j++) {
            y[i] += a[i][j]*x[j];
        }
    }
}

void wrong_order_matvec(float **a, float *x, float *y, int size) {
    for (int j=0; j<size; j++) {
        for (int i=0; i<size; i++) {
            y[i] += a[i][j]*x[j];
        }
    }
}

void tick(struct timeval *t) {
    gettimeofday(t, NULL);
}


double tock(struct timeval *t) {
    struct timeval now;
    gettimeofday(&now, NULL);
    return (double)(now.tv_sec - t->tv_sec) + ((double)(now.tv_usec - t->tv_usec)/1000000.);

}


void freeall(float ***a, float **x, float **y, int size) {
    free2d(a, size);
    free1d(x, size);
    free1d(y, size);
    return;
}

int alloc2d(float ***a, int n) {
    float *data = (float *)malloc(n*n*sizeof(float));
    if (data == NULL) return -1;

    *a = (float **)malloc(n*sizeof(float *));
    if (*a == NULL) {free(data); return -1;};

    for (int i=0; i<n; i++)
        (*a)[i] = &(data[i*n]);

    return 0;
}
int free2d(float ***a, int n) {
    free (&((*a)[0][0]));
    free(*a);

    return 0;
}


int alloc1d(float **a, int n) {
    *a = (float *)malloc(n*sizeof(float));
    if (*a == NULL) return -1;

    return 0;
}

int free1d(float **a, int n) {
    free(*a);

    return 0;
}

运行结果如下:

$ gcc -o papi-test papi-test.c -I${PAPI_INC_DIR} -L${PAPI_LIB_DIR} -lpapi -Wall -std=c99
$ ./papi-test
Test Sensible: time elapsed = 0.121877, memory accesses = 302020775, flop = 33580481
    Memory bandwidth (MB/sec) = 9453.119330
    L1 cache miss rate = 0.003921
    MFLOPS = 262.763624

Test Wrong order: time elapsed = 0.537639, memory accesses = 302026751, flop = 39629352
    Memory bandwidth (MB/sec) = 2142.963254
    L1 cache miss rate = 0.094045
    MFLOPS = 70.295301

获取整个应用程序使用的带宽的命令是什么?我也很感兴趣。 - a3mlord
1
根据您提到的网站,内存带宽计算公式如下:((PAPI_Lx_TCM * Lx_linesize) / PAPI_TOT_CYC) * Clock(MHz),其中Lx为最后一级缓存。我们实际上一直在使用类似的公式:(PAPI_Lx_TCM * Lx_linesize) / Time(s)。然而,我们认为这并没有涵盖预取和缓存写回等对使用带宽也有贡献的因素。(来源) - Cristiano Sousa
1
@Zboson - 计算指令延迟是否更有用?大概通常而言(而且有一个可以计数的接口:PAPI_MEM_SCY),但这完全取决于某人试图回答的问题。如果问题是“是否有调优机会”,那么减少指令延迟和各个级别的缓存未命中是比较好的目标;但如果问题是简单的“我已经达到了硬件极限吗?”测量总带宽也是一个相当合理的地方。 - Jonathan Dursi
1
@Zboson 我们想要测量已使用的内存带宽的主要原因是为了证明增加线程数量不会提高性能,以此来证明我们是否饱和了可用的带宽 (来源) - Cristiano Sousa
1
@JonathanDursi,关于您最后的评论:这是“不可能”的,因为这是一种不规则算法...除此之外,我认为无论我们是否使用整个缓存行,我们都需要测量真实带宽使用值。 - a3mlord
显示剩余12条评论

1
要测量你的应用程序的带宽,你需要知道正在读取和/或写入多少内存,让我们称之为分子,并且你需要知道读取和/或写入它需要多少时间,让我们称之为分母。带宽是分子/分母。
如果你的应用程序很复杂,那么计算正在读取和/或写入多少内存可能不太容易。此外,如果你的应用程序正在执行许多其他操作,那么计算时间可能也不容易。你必须减去其他操作的时间。因此,在测量最大吞吐量时通常使用简单的算法。
如果你想选择一个简单的算法来尝试并与你的应用程序进行比较,那么你应该看看你的应用程序是否只写入数据,只从数据中读取,还是既读又写。
如果你只写入数据,那么你可以使用写入(memset)测试:
#pragam omp parallel for
for(int i=0; i<n; i++) {
    x[i] = k;
}

如果您既读取又写入数据,可以进行简单的复制(memcpy)测试。
#pragma omp parallel for
for(int i=0; i<n; i++) {
    y[i] = x[i];
}

实际上,如果你查看STREAM源代码,这基本上就是它在执行复制测试的方式。
如果你只是读取数据,你可以像这样进行约简(如果要矢量化,请确保使用-ffast-math编译):
#pragma omp parallel for reduction(+:sum)
for(int i=0; i<n; i++) {
    sum += x[i]*y[i];
}

STREAM的测试都是读写测试。我编写了自己的带宽工具,只进行写操作、读写操作和仅读操作。

不幸的是,写入数据的测试无法接近峰值带宽。原因是为了写入数据,它们必须先将数据读入缓存中。这就是为什么在我的系统上STREAM无法接近峰值带宽的原因。为了在进行写操作时获得峰值带宽,需要进行非临时存储,这种存储只会直接写入数据,而不会先将其读入缓存。

例如,使用SSE并假设xy是浮点数数组,可以像这样进行读写测试:

#pragma omp parallel for    
for(int i=0; i<n/4; i++) {
    __m128 v = _mm_load_ps(&x[4*i]);
    _mm_stream_ps(&y[4*i], v);
}

如果你看一下Agner Fog的asmlib,你会发现他正是为大型数组的memsetmemcpy做的这个工作。实际上,他的asmlib和我刚才提到的例子在我的系统上获得了85% (45 GB/s out of 51 GB/s) 的带宽, 而STREAM测试只能获得约45%的带宽
这些测试假设你的算法是内存受限的,并且为了比较,你需要读取一个比最慢的缓存大得多的数组。如果你的算法重复使用仍在缓存中的数据,那么读取测试也无法接近峰值带宽,因为存在循环依赖。要解决这个问题,你需要展开3-10次,具体取决于操作和硬件。此外,如果你正在为适合缓存并将重用的数组进行写入,则不应该进行非临时存储。这就是为什么Agner Fog的asmlib仅对大型数组使用非临时存储的原因。

很抱歉,我不知道如何将您的建议概括到一个可以测量我的应用程序带宽使用情况的程度。我的应用程序非常复杂,有许多并行区域和内核。一些内核中会发生数据重用。我的提示是我的应用程序受到内存限制,但即使这样,我也无法用科学方法支持这种说法,因为我不知道任何方法。 - a3mlord
我只使用整数操作,有没有所谓的GIOPS?顺便问一下,有没有方法可以确定一个应用程序是否受到内存限制,或者我们是凭直觉来指导自己的?谢谢。 - a3mlord
@a3mlord 或许有一种性能分析工具可以告诉你,你的应用程序在读写操作和整数计算方面所花费的时间比例。这可能是确定是否存在内存限制的一种方法。 - Z boson
你能再详细说明一下你的建议吗?使用什么性能分析工具以及如何分析数据?现在,假设我没有使用它,我们可以假定我可以给出我计算的操作数的上限。同时,我们也可以假设这个上限是2^n,其中n是应用程序的输入参数。那么,我该如何得到答案呢?我仍然不知道我每秒从内存中读取多少次,对吧? - a3mlord
1
谢谢Z玻色子,虽然那不是我想要的答案。我正在寻找一种实用的方法来到达那里(包括指定分析工具和如何到达那里)。 - a3mlord
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接