并行编程实战——CUDA编程的打印输出

一、调试的最基本的方法

在前面的CUDA调试中给出了很多种方法,重点介绍了使用IDE相关的调试。但实际在某些情况下,不少开发者还是更青睐直接打印数据结果。特别是可能无法使用IDE的场景下,这种打印可能起着重要的作用。

那么CUDA编程中的打印数据和主机应用中的普通编程打印有什么不一样呢?本文将对其进行重点的分析。

二、CUDA中的printf

在CUDA编程的系列中,前面的环境安装中,给出一个最简单的例子。通过不同的函数来区分执行的过程是在CPU还是在GPU上。可以非常清楚的让普通开发者看清楚代码是否被CUDA框架调用进入到了设备端进行运行。

但这个例程太简单了,简单到极有可能让大家产生误会。就一如人们批评某位C语言的老师误导开发者一样,事实确实也是如此。

首先CUDA中的printf并不完全兼容传统的C语言中的printf,或者说它只是C语言中printf的一个子集(对格式支持和参数数量都有限制,如打印__half类型,必须先转float)。它在引入相关的头文件后,可以直接在内核函数中调用。其次,printf函数的缓冲区是一个有限大小的环形缓冲区(1M左右,当然也可以通过接口对其大小进行设置)。再次,CUDA编程的并行性和异步性,导致了其输出的异步性和缓冲区的刷新机制有所不同,需要使用同步机制进行处理(如cudaDeviceSynchronize()等同步机制)并控制日志输出时不被覆盖。而并行性也可能导致其输出的顺序的非确定性。最后,printf函数应用成本并不低,它可能导致性能的下降。这意味着真正部署的代码中尽量去除这些打印的相关代码。

三、如何正确应用

而在真正的应用中,一般很少直接使用printf函数进行日志输出。更多的是使用宏进行封装,然后再对宏进行应用。在调试和发布间只通过宏的处理即可将相关的调试信息全部去除。一般如下面的情况:

c 复制代码
#define CUDA_DEBUG 1

#if CUDA_DEBUG
    #define DEBUG_PRINT(fmt, args...) printf("DEBUG: " fmt, ##args)
#else
    #define DEBUG_PRINT(fmt, args...)
#endif

__global__ void kerfunc() {
    int tid = threadIdx.x;
    DEBUG_PRINT("cur Thread id %d \n", tid);
}

也可以进一步的封装:

c 复制代码
#include <stdio.h>
#define CUDA_CHECK(call) \
    do { \
        cudaError_t err = call; \
        if (err != cudaSuccess) { \
            fprintf(stderr, "CUDA Error: %s (%s:%d)\n", \
                    cudaGetErrorString(err), __FILE__, __LINE__); \
            exit(EXIT_FAILURE); \
        } \
    } while(0)

在一些开源的代码中经常可以看到上类似的代码,而且它们看上去和在CPU中打印没有本质的不同,只是运行的位置不同罢了。

四、例程

基础的应用往往能够解决一些细节的问题,所以对printf还是得专门搞个例子来看看:

c 复制代码
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include <stdlib.h>

#include <stdio.h>
#define CUDA_CHECK(call) \
    do { \
        cudaError_t err = call; \
        if (err != cudaSuccess) { \
            fprintf(stderr, "CUDA Error: %s (%s:%d)\n", \
                    cudaGetErrorString(err), __FILE__, __LINE__); \
            exit(EXIT_FAILURE); \
        } \
    } while(0)

__global__ void printfTest()
{
    int tid = threadIdx.x + blockIdx.x * blockDim.x;
    printf("[Block %d, Thread %d] => Global thread ID: %d\n",
        blockIdx.x, threadIdx.x, tid);
}

int main()
{
    CUDA_CHECK(cudaDeviceSetLimit(cudaLimitPrintfFifoSize, 16 * 1024 * 1024)) ;

    int blocks = 2;
    int threads = 4;

    printfTest << <blocks, threads >> > ();

    CUDA_CHECK(cudaGetLastError());
    CUDA_CHECK(cudaDeviceSynchronize());

    printf("\nkernel printf output end.\n");

    return 0;
}

例子确实不复杂,但基本上还是比较清楚的描述了printf的应用方法和相关的初步封装。

五、总结

在前面的调试中其实是忽略了对printf的应用分析。但此函数还是相当重要的的。如果应用得当,它可以起到关键的作用。让开发者能够很快的找到关心的细节结果。

相关推荐
程序leo源2 小时前
Qt信号与槽深度详解
c语言·开发语言·数据库·c++·qt·c#
水云桐程序员2 小时前
C++数组详细介绍
开发语言·c++
z200509302 小时前
今日算法(二叉树)
数据结构·c++·算法
故事和你912 小时前
洛谷-【图论2-2】最短路1
开发语言·数据结构·c++·算法·动态规划·图论
杰之行2 小时前
Fast-DDS 接收数据完整时序分析
c++·人工智能
沫璃染墨3 小时前
红黑树完全指南:从核心原理到插入验证全实现
开发语言·c++·算法
号码认证服务3 小时前
客户看到来电显示公司名会更愿意接听吗?企业号码认证提升ROI
服务器·网络·c++·经验分享·智能手机·云计算·php
流年如夢3 小时前
初入C++
开发语言·c++
yoyo_zzm3 小时前
编程语言大比拼:C++到PHP全解析
开发语言·c++·php