全局内存上的连续CUDA原子操作是否可以从L2缓存中受益?

时间:2014-05-19 18:44:28

标签: caching cuda gpu gpgpu atomic

在启用缓存的CUDA设备中,一个线程对全局内存地址的连续原子操作中的引用的位置是否受益于L2缓存?
例如,我在CUDA内核中使用返回值进行原子操作。

uint a = atomicAnd( &(GM_addr[index]), b );

我在想是否要再次使用相同内核中的线程使用atomic,如果我可以将新原子操作的地址限制为32字节长[ &(GM_addr[index&0xFFFFFFF8]), &(GM_addr[index|7]) ]间隔,我会有L2缓存中的命中(具有32字节长的缓存行)。这种猜测是否正确?或者是否有与全局原子相关的例外?

1 个答案:

答案 0 :(得分:1)

我在这里回答分享我的方法,以找出二级缓存利用率对全局原子的影响。我不接受这个答案,因为从架构的角度来看,我不认为自己还没有意识到L2缓存上的原子会发生什么。

我创建了一个简单的CUDA程序。

#include <stdio.h>

static void HandleError( cudaError_t err, const char *file, int line ) {
    if (err != cudaSuccess) {
        fprintf( stderr, "%s in %s at line %d\n", cudaGetErrorString( err ), file, line );
        exit( EXIT_FAILURE );
    }
}
#define HANDLE_ERROR( err ) (HandleError( err, __FILE__, __LINE__ ))

__global__ void address_confined(uint* data, uint nElems) {
    uint tmp, a = 1;
    for(    uint index = 0;
            index < nElems;
            ++index ) {
        tmp = data[index];
        data[index] += a;
        a = tmp;
    }
}

__global__ void address_not_confined(uint* data, uint nElems) {
    uint tmp, a = 1;
    for(    uint index = 0;
            index < nElems;
            index += 8  ) {
        tmp = data[index];
        data[index] += a;
        a = tmp;
    }
}

__global__ void address_confined_atomics(uint* data, uint nElems) {
    uint a = 1;
    for(    uint index = 0;
            index < nElems;
            ++index ) {
        a = atomicAdd ( &(data[index]), a);
    }
}

__global__ void address_not_confined_atomics(uint* data, uint nElems) {
    uint a = 1;
    for(    uint index = 0;
            index < nElems;
            index += 8  ) {
        a = atomicAdd ( &(data[index]), a);
    }
}

int main ( ){

    const unsigned int nElems = 1 << 23;

    unsigned int* dev_data;
    HANDLE_ERROR( cudaMalloc((void**) &(dev_data), (nElems) * sizeof(unsigned int)) );
    HANDLE_ERROR( cudaMemset(dev_data, 0, nElems) );

    cudaEvent_t start, stop;
    HANDLE_ERROR( cudaEventCreate(&start) );
    HANDLE_ERROR( cudaEventCreate(&stop) );
    float dt_ms;

    HANDLE_ERROR( cudaEventRecord(start) );
    address_confined<<<1,1>>>(dev_data, nElems>>3);
    HANDLE_ERROR( cudaPeekAtLastError() );
    HANDLE_ERROR( cudaEventRecord(stop) );
    HANDLE_ERROR( cudaDeviceSynchronize() );
    HANDLE_ERROR( cudaEventElapsedTime(&dt_ms, start, stop) );
    fprintf( stdout, "Address-confined global access took %f (ms).\n", dt_ms);

    HANDLE_ERROR( cudaEventRecord(start) );
    address_not_confined<<<1,1>>>(dev_data, nElems);
    HANDLE_ERROR( cudaPeekAtLastError() );
    HANDLE_ERROR( cudaEventRecord(stop) );
    HANDLE_ERROR( cudaDeviceSynchronize() );
    HANDLE_ERROR( cudaEventElapsedTime(&dt_ms, start, stop) );
    fprintf( stdout, "Address-NOT-confined global access took %f (ms).\n", dt_ms);

    HANDLE_ERROR( cudaEventRecord(start) );
    address_confined_atomics<<<1,1>>>(dev_data, nElems>>3);
    HANDLE_ERROR( cudaPeekAtLastError() );
    HANDLE_ERROR( cudaEventRecord(stop) );
    HANDLE_ERROR( cudaDeviceSynchronize() );
    HANDLE_ERROR( cudaEventElapsedTime(&dt_ms, start, stop) );
    fprintf( stdout, "Address-confined atomics took %f (ms).\n", dt_ms);

    HANDLE_ERROR( cudaEventRecord(start) );
    address_not_confined_atomics<<<1,1>>>(dev_data, nElems);
    HANDLE_ERROR( cudaPeekAtLastError() );
    HANDLE_ERROR( cudaEventRecord(stop) );
    HANDLE_ERROR( cudaDeviceSynchronize() );
    HANDLE_ERROR( cudaEventElapsedTime(&dt_ms, start, stop) );
    fprintf( stdout, "Address-NOT-confined atomics took %f (ms).\n", dt_ms);

    HANDLE_ERROR( cudaFree(dev_data) );
    return(EXIT_SUCCESS);

}

在上面的四个内核中,只有一个活动线程尝试对全局内存中的整数执行读 - 修改 - 写。我选择了一个线程以消除其他线程可能产生的影响。两个内核使用32字节跃点来跳过已经缓存在L2中的内容,另外两个内核访问连续的整数。两个内核使用原子,两个不使用 我使用CUDA 6.0在Ubuntu 12.04中为CC = 3.5和-O3标志编译它。我在GeForce GTX 780(Kepler GK110)上运行它。

我得到了以下结果:

Address-confined global access took 286.206207 (ms).
Address-NOT-confined global access took 398.450348 (ms).
Address-confined atomics took 231.808640 (ms).
Address-NOT-confined atomics took 349.534637 (ms).

从上面的结果可以看出,与对普通全局内存访问的影响相比,L2对原子的影响相等甚至更大。

我从分析原子内核获得了以下结果:

-- address_not_confined_atomics --
L2 Write Transactions: 1048582
L2 Read Transactions: 1069849
Device Memory Write Transactions: 1048578
Device Memory Read Transactions: 1877877
L2 Throughput (Writes): 96.753 (MB/s)
L2 Throughput (Reads): 98.716 (MB/s)

-- address_confined_atomics --
L2 Write Transactions: 1048581
L2 Read Transactions: 1061095
Device Memory Write Transactions: 1046652
Device Memory Read Transactions: 672616
L2 Throughput (Writes): 147.380 (MB/s)
L2 Throughput (Reads): 149.139 (MB/s)

我没有在此处提供非原子性分析结果,因为它们或多或少类似于上面的相应版本。在我看来,性能增益来自L2缓存吞吐量增强。特别是当内核执行时间减少的程度与L2缓存吞吐量的增加成比例时。原子版本和非原子版本的L2缓存减少了设备全局内存所需的读取事务数量,从而减少了整体读取延迟。回顾一下,它似乎与原子操作的非原子访问(使用返回值的访问)一样重要,以便在全局内存引用中具有局部性。 谨防不使用返回值的原子产生不同的设备指令;因此,不能依赖上述评估。