前几天做实验的时候写了一段程序,测试完成后,需要把这段程序从CPU上移植到GPU上。本来想着这活不会太难,可能半天时间就搞定了吧,没想到已经花了三天多的时间了,磕磕绊绊的还没彻底结束,不过GPU的程序总算已经有个样子了,于是打算抽时间把这几天的血泪经验总结一下,那就是:不!要!使!用!二!级!指!针!!!!!
(平静一下心情~~~)
我们先从内存说起吧。每一个内存单元都包含两个属性,一个是“地址”,一个是“内容”。把内存比喻成许多抽屉组成的柜子,那一个内存单元就相当于柜子里面的小抽屉,每个小抽屉有两个属性,一个它能装东西(内容),二是它有个抽屉编号(地址)。比如下面就是一个内存的示意图,每一个小方格是一个内存单元,浅蓝是它的内容,深蓝是它的地址:
内存的地址是固定的,没法改变。比如上面的小方格地址表示为在第几行第几列,我们没法改变。程序猿可以控制的是内存的内容。一种特殊的情况就是,如果一个内存单元的内容是别的内存的地址,那么它就被称为指针,比如下图中,左边的内存单元就被称为指针,因为它的内容,是右边内存单元的地址。我们称这种情况为左边的指针指向了右边的内存单元。
如果我们任性一点,让一个指针指向另一个指针,即所谓的螳螂捕蝉黄雀在后,那么就得到了一个二级指针,如下图所示。以此类推,我们理论上可以定义三级,四级等多级指针(不过现实中我还没见过什么地方要用到四级指针)
每一本C语言的教科书都会讲到指针,而且还会特别声明指针是有害的,尽量别用它。猜猜为什么?因为它太灵活了,指针在内存中变来变去,如果程序稍微复杂一点,那么人脑很可能就顾不过来了,说不定哪个指针就变成了被遗忘的孩子,到处惹是生非。有些高级语言已经干脆取消了指针。不过就像前面说的,指针灵活呀,有些事情用它很方便就能完成了。所以说呢,每一个程序猿需要在灵活性与安全性之间权衡取舍一番。
好,介绍完了背景知识,现在该说说我遇到的问题了。在用C++写程序的时候定义了一个类Sample,头文件是:
#include"stdio.h"
#include"InstanceList.h"
usingnamespacestd;
classSample
{
public:
Sample();
int* Data;
intAttributesCount;
intAttributesInLastInt;
intIntPerInstance;
intTrueInstanceCount;
intSize;
};
其中有一个成员变量Data,它是一个int型指针,顾名思义是用来存储数据的。由于数据很多,直接传内容特别浪费内存和时间,所以整个程序中只保留了一个备份。每次有函数需要用这些数据的时候,就传Sample的地址(也就是它的指针)。用Sample的地址读取像IntPerInstance这样的变量的时候,它就是指针,但要注意,读取Data的时候,它就变成了二级指针了。
虽然用到了二级指针,但我处理的比较小心,程序在CPU上能够正常运行,不过移植到GPU的时候,运行一下就出现了段错误(段错误的意思是,我的指针指向了程序不能够使用的内存):
为什么会这样呢?因为使用GPU的时候,程序的内存包括两部分,一部分位于CPU当中,一部分位于GPU当中,而由于CPU和GPU是两块独立的设备,他们的指针是不能指向对方的地址的,比如下图的情况,就绝对禁止。
所以,本来好好的程序,因为指针的原因,就可能发生各种段错误。说到这里,其实只说清楚了在写GPU程序的时候,“使用指针容易引发错误”,但题目里为什么强调是二级指针的问题呢?
因为在GPU当中,内存还细分为global memory和shared memory,每次用指针的时候都要确定是global还是shared类型的。虽然编译器从二级指针定义的地方,可以很方便的知道它自身是哪种类型的,不过它指向的一级指针随时都可能发生变动,很难检查出来到底是什么指针,于是编译器就要报警告了,就像下面:
所有这些报警告的地方,都是因为使用了samplepointer->Data[address]这样的语句。编译器确定samplepointer是一个global类型的指针之后,再也无力确定Data是什么类型的指针了,只好假设Data是一个global类型的指针。虽然由于别的原因,这种假设有99.99%的正确率,不过看着这么多警告,一来危险,二来着实让人不爽啊~
目前这些警告并不会影响我程序的正常运行,但后面还要加新的功能,很不方便。网上有人提到了一两种可行的解决办法,我试了没效果,于是这几天花大力气把代码结构改了,删了Sample类,用一个数组代替,整个过程还是很耗时间的。