float3可以享受CUDA内存合并吗?

时间:2019-04-15 11:34:41

标签: cuda

根据我的理解,每个线程仅按4个字节,8个字节或16个字节访问内存才可以享受CUDA全局内存合并。在此之后,经常使用的float3是 6 12字节类型,因此无法合并。我说的对吗?

1 个答案:

答案 0 :(得分:2)

tl; dr:float3的概念在发生合并的级别上不存在。因此,是否将合并float3的问题实际上不是要提出的正确问题。至少一般而言,这不是一个可以回答的问题。可以回答 的问题是:“此特定内核使用float3以这种特定方式使用的特定内核生成的负载/存储最终会合并吗?”不幸的是,甚至只有通过查看机器代码,最重要的是,剖析,才能真正回答这个问题。


当前所有的CUDA架构都支持1字节,2字节,4字节,8字节和16字节的全局内存加载和存储。在此重要的是要理解,这并不意味着例如通过某种其他机制进行假设的12字节加载/存储。这意味着可以通过1字节,2字节,4字节,8字节或16字节的加载和存储访问全局存储器。就是这样;期。除了通过这些1字节,2字节,4字节,8字节或16字节的加载和存储,没有 no 方式来访问全局内存。特别是,没有12字节的加载和存储。

float3是在CUDA C ++语言级别存在的抽象。硬件对float3应该是什么一无所知。硬件对于全局内存的全部了解是,您可以一次加载或存储1、2、4、8或16字节。 CUDA C ++ float3 consists of三个浮点数。 float(在CUDA中)为4字节宽。因此,访问float3的元素通常只映射到4字节加载/存储。访问float3的所有元素通常会导致三个4字节的加载/存储。例如:

__global__ void test(float3* dest)
{
    dest[threadIdx.x] = { 1.0f, 2.0f, 3.0f };
}

如果look at the PTX assembly编译器为此内核生成,则会看到将{ 1.0f, 2.0f, 3.0f }分配给我们的float3编译为三个4字节存储区:

    mov.u32         %r2, 1077936128;
    st.global.u32   [%rd4+8], %r2;
    mov.u32         %r3, 1073741824;
    st.global.u32   [%rd4+4], %r3;
    mov.u32         %r4, 1065353216;
    st.global.u32   [%rd4], %r4;

这些只是正常的加载/存储,与其他任何加载/存储一样,没有什么特别的。这些单独的负载/存储库可能像其他任何负载/存储库一样可能合并。在此特定示例中,内存访问模式将如下所示:

1st store:  xx xx t1 xx xx t2 xx xx t3 xx xx t4 xx xx t5 xx xx t6 …
2nd store:  xx t1 xx xx t2 xx xx t3 xx xx t4 xx xx t5 xx xx t6 xx …
3rd store:  t1 xx xx t2 xx xx t3 xx xx t4 xx xx t5 xx xx t6 xx xx …

其中t i 是经线的第 i 个线程,而xx表示跳过的4字节地址。如您所见,我们的线程执行的存储之间存在8字节的间隙。但是,仍然有相当多的4字节存储区都属于同一128字节高速缓存行。因此,访问模式仍然允许 some 合并(在任何当前体系结构上),这与理想情况相去甚远。但是有些总比没有好。有关更多详细信息,请参见the CUDA documentation

请注意,所有这些实际上仅取决于最终生成的机器代码所产生的内存访问模式。是否(以及是否可以)将存储器访问合并到何种程度与在C ++级别使用特定数据类型无关。为了说明这一点,请考虑以下示例:

struct Stuff
{
    float3 p;
    int blub;
};

__global__ void test(Stuff* dest)
{
    dest[threadIdx.x].p = { 1.0f, 2.0f, 3.0f };
    dest[threadIdx.x].blub = 42;
}

Looking at the PTX assembly,我们看到编译器将此C ++代码转换为四个单独的4字节存储。到目前为止没有任何惊喜。让我们稍微修改一下代码

struct alignas(16) Stuff
{
    float3 p;
    int blub;
};

__global__ void test(Stuff* dest)
{
    dest[threadIdx.x].p = { 1.0f, 2.0f, 3.0f };
    dest[threadIdx.x].blub = 42;
}

,请注意,突然之间,编译器has turned all of this into a single 16-Byte store。知道Stuff对象将始终位于16字节边界,并且根据C ++语言的规则,此处的struct成员的各个修改不能以任何特定的顺序被另一个线程(即线程)观察到。编译器可以将所有这些分配融合到一个16字节存储中,最终导致访问模式类似

t1 t1 t1 t1 t2 t2 t2 t2 t3 t3 t3 t3 t4 t4 t4 t4 …

另一个例子:

__global__ void test(float3* dest)
{
    auto i = threadIdx.x % 3;
    auto m = i == 0 ? &float3::x : i == 1 ? &float3::y : &float3::z;
    dest[threadIdx.x / 3].*m = i;
}

在这里,我们再次写入float3数组。但是,每个线程will perform恰好一个存储到float3的成员之一,并且连续的线程将存储到连续的4字节地址,从而导致完美的内存访问:

t1 t2 t3 t4 t5 t6 t7 t8 t9 t10 t11 t12 t13 t14 t15 …

同样,我们的C ++代码有时在使用float3的事实本身是完全不相关的。重要的是我们实际上在做什么,产生了什么负载/存储,以及结果是什么样的访问模式……