float3能享受CUDA记忆融合吗?

float3能享受CUDA记忆融合吗?,cuda,Cuda,据我所知,每个线程只能访问4字节、8字节或16字节的内存,才能享受CUDA全局内存聚合的乐趣。接下来,经常使用的float3是612字节类型,不适用于合并。我说得对吗;dr:float3的概念不存在于合并发生的级别。因此,一个float3是否会合并的问题实际上不是一个正确的问题。至少总的来说,这不是一个可以回答的问题。一个可以回答的问题是:“这个以这种特殊方式使用float3的特定内核生成的加载/存储是否会最终合并?”不幸的是,即使是这个问题也只能通过查看机器代码,最重要的是,分析来真正回答

据我所知,每个线程只能访问4字节、8字节或16字节的内存,才能享受CUDA全局内存聚合的乐趣。接下来,经常使用的float3是612字节类型,不适用于合并。我说得对吗;dr:float3的概念不存在于合并发生的级别。因此,一个
float3
是否会合并的问题实际上不是一个正确的问题。至少总的来说,这不是一个可以回答的问题。一个可以回答的问题是:“这个以这种特殊方式使用
float3
的特定内核生成的加载/存储是否会最终合并?”不幸的是,即使是这个问题也只能通过查看机器代码,最重要的是,分析来真正回答


所有当前CUDA体系结构都支持1字节、2字节、4字节、8字节和16字节全局内存加载和存储。这里必须理解,这并不意味着,例如,假设的12字节加载/存储将通过其他机制发生。这意味着可以通过1字节、2字节、4字节、8字节或16字节的加载和存储访问全局内存。就这样;时期除了通过这些1字节、2字节、4字节、8字节或16字节加载和存储之外,没有其他方法可以访问全局内存。特别是,没有12字节的加载和存储

<代码> FLUAT3是CUDA C++语言层存在的抽象。硬件根本不知道什么是

float3
。当涉及到全局内存时,硬件所能理解的是,您可以一次加载或存储1、2、4、8或16个字节。CUDA C++ ++代码>浮标3<代码>三浮点数。
float
(在CUDA中)的宽度为4字节。因此,访问
float3
的元素通常只会映射到4字节的加载/存储。访问
float3
的所有元素通常会导致三个4字节的加载/存储。例如:

__global__ void test(float3* dest)
{
    dest[threadIdx.x] = { 1.0f, 2.0f, 3.0f };
}
__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;
}
如果编译器为此内核生成,您将看到将
{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 …
其中ti是warp的第i条线程,
xx
表示跳过的4字节地址。如您所见,线程执行的存储之间有8字节的间隔。然而,仍然有相当多的4字节存储都属于同一个128字节缓存线。因此,访问模式仍然允许一些合并(在任何当前体系结构上),这离理想还很远。但有总比没有好。有关这方面的更多详细信息,请参阅

请注意,所有这些实际上完全取决于生成的机器代码最终会产生什么内存访问模式。无论是否,如果是,可以将何种程度的内存访问合并在一起,与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;
}

,我们看到编译器将这个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;
}
请注意,突然,编译器。知道一个<代码> Test> 对象被保证总是处于16字节边界,并且根据C++语言的规则,这里的结构成员的个别修改不能以任何特定的顺序被另一个线程观察到,编译器可以将所有这些任务融合成一个16字节的存储,这最终导致了一种访问模式,如

t1 t1 t1 t1 t2 t2 t2 t2 t3 t3 t3 t3 t4 t4 t4 t4 …
另一个例子:

__global__ void test(float3* dest)
{
    dest[threadIdx.x] = { 1.0f, 2.0f, 3.0f };
}
__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
数组。但是,每个线程只对
float3
的一个成员存储一个数据,连续的线程将存储到连续的4字节地址,从而实现完美的合并内存访问:

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

再次,事实上,我们的C++代码在某个时候使用了<代码> FLUAT3本身完全无关。相关的是我们实际在做什么,生成了什么加载/存储,以及访问模式的结果是什么…

float3
是一个12字节的类型。你说得对,罗伯特,这有点尴尬哈哈哈