65.9K
CodeProject 正在变化。 阅读更多。
Home

通过包装器轻松实现 SIMD

2015年5月13日

CPOL

16分钟阅读

viewsIcon

17599

本文旨在改变您对如何在代码中应用 SIMD 编程的看法。

Intel® Developer Zone 提供跨平台应用开发工具和操作指南、平台及技术信息、代码示例以及同行专业知识,帮助开发人员创新和取得成功。加入我们的社区,了解 物联网Android*Intel® RealSense™ TechnologyWindows*,下载工具、访问开发套件、与志同道合的开发人员交流想法,并参与黑客马拉松、竞赛、路演和本地活动。

1. 引言

本文旨在改变您对如何在代码中应用 SIMD 编程的看法。通过将 SIMD 通道视为与 CPU 线程功能相似,您将获得新的见解,并能够在代码中更频繁地应用 SIMD。

Intel 生产支持 SIMD 的 CPU 的时间大约是生产多核 CPU 时间的两倍,但线程在软件开发中的普及程度更高。支持度增加的一个原因是,有大量的教程以简单的“运行此入口函数 n 次”方式介绍线程,忽略了所有潜在的陷阱。另一方面,SIMD 教程往往侧重于实现最终的 10% 加速,这需要将代码量增加一倍。如果这些教程提供了代码作为示例,您可能会发现很难专注于所有新信息,同时还能想出自己简单优雅的使用方法。因此,展示一种简单、实用的 SIMD 使用方法是本文的主题。

首先是 SIMD 代码的基本原理:对齐。几乎所有 SIMD 硬件都要求或至少偏好某种自然对齐,解释这些基础知识本身就可以写一篇论文[1]。但总的来说,如果您不担心内存不足,以缓存友好的方式分配内存非常重要。对于 Intel CPU 来说,这意味着在 64 字节边界上分配内存,如代码片段 1 所示。

inline void* operator new(size_t size)
{
	return _mm_malloc(size, 64);
}

inline void* operator new[](size_t size)
{
	return _mm_malloc(size, 64);
}

inline void operator delete(void *mem)
{
	_mm_free(mem);
}

inline void operator delete[](void *mem)
{
	_mm_free(mem);
}
代码片段 1:尊重缓存友好 64 字节边界的分配函数

2. 基本思想

开始的方法很简单:假设 SIMD 寄存器的每个通道都像一个线程一样执行。对于 Intel® Streaming SIMD Extensions (Intel® SSE),您有 4 个线程/通道;对于 Intel® Advanced Vector Extensions (Intel® AVX),有 8 个线程/通道;而对于 Intel® Xeon-p Phi 协处理器,则有 16 个线程/通道。

为了获得“即插即用”的解决方案,第一步是实现行为上与基本数据类型几乎相同的类。包装“int”、“float”等,并将这些包装器用作每个 SIMD 实现的起点。对于 Intel SSE 版本,将 float 成员替换为 __m128,将 int 和 unsigned int 替换为 __m128i,并使用 Intel SSE intrinsics 或 Intel AVX intrinsics 实现运算符,如代码片段 2 所示。

// SEE 128-bit
inline	DRealF	operator+(DRealF R)const{return DRealF(_mm_add_ps(m_V, R.m_V));}
inline	DRealF	operator-(DRealF R)const{return DRealF(_mm_sub_ps(m_V, R.m_V));}
inline	DRealF	operator*(DRealF R)const{return DRealF(_mm_mul_ps(m_V, R.m_V));}
inline	DRealF	operator/(DRealF R)const{return DRealF(_mm_div_ps(m_V, R.m_V));}

// AVX 256-bit
inline	DRealF	operator+(const DRealF& R)const{return DRealF(_mm256_add_ps(m_V, R.m_V));}
inline	DRealF	operator-(const DRealF& R)const{return DRealF(_mm256_sub_ps(m_V, R.m_V));}
inline	DRealF	operator*(const DRealF& R)const{return DRealF(_mm256_mul_ps(m_V, R.m_V));}
inline	DRealF	operator/(const DRealF& R)const{return DRealF(_mm256_div_ps(m_V, R.m_V));}
代码片段 2:SIMD 包装器的重载算术运算符

3. 使用示例

现在,假设您正在处理两个 HDR 图像,其中每个像素都是一个 float,并且您需要在两者之间进行混合。

void CrossFade(float* pOut,const float* pInA,const float* pInB,size_t PixelCount,float Factor)

void CrossFade(float* pOut,const float* pInA,const float* pInB,size_t PixelCount,float Factor)
{
	const DRealF BlendA(1.f - Factor);
	const DRealF BlendB(Factor);
	for(size_t i = 0; i < PixelCount; i += THREAD_COUNT)
		*(DRealF*)(pOut + i) = *(DRealF*)(pInA + i) * BlendA + *(DRealF*)(pInB + i) + BlendB;
}
代码片段 3:可与基本数据类型和 SIMD 数据一起使用的混合函数

从代码片段 3 生成的可执行文件既可以在普通寄存器上原生运行,也可以在 Intel SSE 和 Intel AVX 上运行。这并不是您通常会写的标准写法,但每个 C++ 程序员仍然应该能够阅读和理解它。让我们看看它是否符合您的预期。实现的第一行和第二行通过复制参数来初始化我们线性插值的混合因子,以适应您的 SIMD 寄存器的宽度。

第三行几乎是一个普通的循环。唯一特殊的部分是“THREAD_COUNT”。对于普通寄存器,它是 1;对于 Intel SSE,它是 4;对于 Intel AVX,它是 8,代表寄存器通道的数量,在我们这个例子中相当于线程数。

第四行索引到数组,并且两个输入像素都按混合因子缩放并相加。根据您的写作偏好,您可能需要使用一些临时变量,但不需要查找任何 intrinsic,也没有每个平台都需要实现的特定代码。

4. 即插即用

现在是时候证明它确实有效了。让我们采用一个标准的 MD5 哈希实现,并利用我们所有的 CPU 功耗来寻找原像。为此,我们将把基本类型替换为我们的 SIMD 类型。MD5 运行几个“轮次”,这些轮次对无符号整数应用各种简单的位操作,如代码片段 4 所示。

#define LEFTROTATE(x, c) (((x) << (c)) | ((x) >> (32 - (c))))
#define BLEND(a, b, x) SelectBit(a, b, x)

template<int r>
inline DRealU Step1(DRealU a,DRealU b,DRealU c,DRealU d,DRealU k,DRealU w)
{
	const DRealU f = BLEND(d, c, b);
	return b + LEFTROTATE((a + f + k + w), r); 
}

template<int r>
inline DRealU Step2(DRealU a,DRealU b,DRealU c,DRealU d,DRealU k,DRealU w)
{
	const DRealU f = BLEND(c, b, d);
	return b + LEFTROTATE((a + f + k + w),r);
}

template<int r>
inline DRealU Step3(DRealU a,DRealU b,DRealU c,DRealU d,DRealU k,DRealU w)
{
	DRealU f = b ^ c ^ d;
	return b + LEFTROTATE((a + f + k + w), r);
}

template<int r>
inline DRealU Step4(DRealU a,DRealU b,DRealU c,DRealU d,DRealU k,DRealU w)
{
	DRealU f = c ^ (b | (~d));
	return b + LEFTROTATE((a + f + k + w), r);
}
代码片段 4:MD5 步骤函数,用于 SIMD 包装器

除了类型名称之外,实际上只有一个变化看起来有点像魔法——“SelectBit”。如果 x 的某个位被设置,则返回 b 的相应位;否则,返回 a 的相应位;换句话说,就是一种混合。主 MD5 哈希函数显示在代码片段 5 中。

inline void MD5(const uint8_t* pMSG,DRealU& h0,DRealU& h1,DRealU& h2,DRealU& h3,uint32_t Offset)
{
	const DRealU w0  =	Offset(DRealU(*reinterpret_cast<const uint32_t*>(pMSG + 0 * 4) + Offset));
	const DRealU w1  =	*reinterpret_cast<const uint32_t*>(pMSG + 1 * 4);
	const DRealU w2  =	*reinterpret_cast<const uint32_t*>(pMSG + 2 * 4);
	const DRealU w3  =	*reinterpret_cast<const uint32_t*>(pMSG + 3 * 4);
	const DRealU w4  =	*reinterpret_cast<const uint32_t*>(pMSG + 4 * 4);
	const DRealU w5  =	*reinterpret_cast<const uint32_t*>(pMSG + 5 * 4);
	const DRealU w6  =	*reinterpret_cast<const uint32_t*>(pMSG + 6 * 4);
	const DRealU w7  =	*reinterpret_cast<const uint32_t*>(pMSG + 7 * 4);
	const DRealU w8  =	*reinterpret_cast<const uint32_t*>(pMSG + 8 * 4);
	const DRealU w9  =	*reinterpret_cast<const uint32_t*>(pMSG + 9 * 4);
	const DRealU w10 =	*reinterpret_cast<const uint32_t*>(pMSG + 10 * 4);
	const DRealU w11 =	*reinterpret_cast<const uint32_t*>(pMSG + 11 * 4);
	const DRealU w12 =	*reinterpret_cast<const uint32_t*>(pMSG + 12 * 4);
	const DRealU w13 =	*reinterpret_cast<const uint32_t*>(pMSG + 13 * 4);
	const DRealU w14 =	*reinterpret_cast<const uint32_t*>(pMSG + 14 * 4);
	const DRealU w15 =	*reinterpret_cast<const uint32_t*>(pMSG + 15 * 4);

	DRealU a = h0;
	DRealU b = h1;
	DRealU c = h2;
	DRealU d = h3;

	a = Step1< 7>(a, b, c, d, k0, w0);
	d = Step1<12>(d, a, b, c, k1, w1);
	.
	.
	.
	d = Step4<10>(d, a, b, c, k61, w11);
	c = Step4<15>(c, d, a, b, k62, w2);
	b = Step4<21>(b, c, d, a, k63, w9);

	h0 += a;
	h1 += b;
	h2 += c;
	h3 += d;
}
代码片段 5:主 MD5 函数

代码的大部分内容又回到了像普通 C 函数一样,除了第一行通过复制我们用传入的参数准备的 SIMD 寄存器来准备数据。在这种情况下,我们用要哈希的数据加载 SIMD 寄存器。一个特殊之处是“Offset”调用,因为我们不希望每个 SIMD 通道执行完全相同的工作,所以这个调用会根据通道索引来偏移寄存器。这就像您会添加的线程 ID 一样。请参见代码片段 6 作为参考。

Offset(Register)
{
	for(i = 0; i < THREAD_COUNT; i++)
		Register[i] += i;
}
代码片段 6:Offset 是一个用于处理不同寄存器宽度的实用函数

这意味着,我们要哈希的第一个元素对于 Intel SSE 来说不是 [0, 0, 0, 0],对于 Intel AVX 来说不是 [0, 0, 0, 0, 0, 0, 0, 0]。取而代之的是,第一个元素分别是 [0, 1, 2, 3] 和 [0, 1, 2, 3, 4, 5, 6, 7]。这复制了通过 4 或 8 个线程/核心并行运行该函数的效果,但在 SIMD 的情况下,是指令并行。

我们可以在表 1 中看到我们花费 10 分钟时间对该函数进行 SIMD 化后得到的结果。

表 1:原始数据类型和 SIMD 数据类型的 MD5 性能

类型 时间 加速比

x86 整数

379.389 秒

1.0 倍

SSE4

108.108 秒

3.5 倍

AVX2

51.490 秒

7.4 倍

 

5. 简单 SIMD 线程的进阶

结果是令人满意的,但并不是线性扩展,因为总有一些非线程部分(您可以在提供的源代码中轻松识别它们)。但我们并不追求通过两倍的工作来实现最后的 10% 的提升。作为程序员,您可能会倾向于选择其他快速解决方案来最大化收益。总会浮现一些考虑因素,例如:循环展开是否有意义?

MD5 哈希似乎经常依赖于先前操作的结果,这对 CPU 流水线不太友好,但如果您展开循环,可能会导致寄存器溢出。我们的包装器可以帮助我们轻松评估这一点。循环展开是超线程的软件版本,我们通过重复处理双倍于 SIMD 通道可用数据的操作来模拟双倍线程的运行。因此,创建一个类似的复制类型,并在内部通过为我们的基本运算符复制每个操作来实现循环展开,如代码片段 7 所示。

struct __m1282
{
	__m128		m_V0;
	__m128		m_V1;
	inline		__m1282(){}
	inline		__m1282(__m128 C0, __m128 C1):m_V0(C0), m_V1(C1){}
};

inline	DRealF	operator+(DRealF R)const
	{return __m1282(_mm_add_ps(m_V.m_V0, R.m_V.m_V0),_mm_add_ps(m_V.m_V1, R.m_V.m_V1));}
inline	DRealF	operator-(DRealF R)const
	{return __m1282(_mm_sub_ps(m_V.m_V0, R.m_V.m_V0),_mm_sub_ps(m_V.m_V1, R.m_V.m_V1));}
inline	DRealF	operator*(DRealF R)const
	{return __m1282(_mm_mul_ps(m_V.m_V0, R.m_V.m_V0),_mm_mul_ps(m_V.m_V1, R.m_V.m_V1));}
inline	DRealF	operator/(DRealF R)const
	{return __m1282(_mm_div_ps(m_V.m_V0, R.m_V.m_V0),_mm_div_ps(m_V.m_V1, R.m_V.m_V1));}
代码片段 7:这些运算符被重新实现,以便同时处理两个 SSE 寄存器

就是这样,现在我们可以再次运行 MD5 哈希函数的计时。

表 2:循环展开 SIMD 数据类型的 MD5 性能

类型 时间 加速比

x86 整数

379.389 秒

1.0 倍

SSE4

108.108 秒

3.5 倍

SSE4 x2

75.659 秒

4.8 倍

AVX2

51.490 秒

7.4 倍

AVX2 x2

36.014 秒

10.5 倍

表 2 中的数据显示,展开循环显然是值得的。我们实现了超越 SIMD 通道数扩展的性能提升,这可能是因为 x86 整数版本已经因为操作依赖性而使流水线停滞。

6. 更复杂的 SIMD 线程

到目前为止,我们的示例很简单,因为代码通常是手动向量化的好候选。除了大量的计算密集型操作之外,没有其他复杂之处。但我们如何处理更复杂的情况,比如分支呢?

解决方案依然很简单且应用广泛:投机计算和掩码。特别是如果您使用过着色器或计算语言,您很可能已经遇到过这种情况。让我们看一段代码片段 8 的基本分支,并将其重写为代码片段 9 中的 `?:` 运算符。

int a = 0;
if(i % 2 == 1)
	a = 1;
else
	a = 3;
代码片段 8:使用 if-else 计算掩码
int a = (i % 2) ? 1 : 3;
代码片段 9:使用三元运算符 ? 计算掩码

如果您还记得代码片段 4 中的位选择运算符,我们也可以用它来实现相同的功能,只进行位运算,如代码片段 10 所示。

int Mask = (i % 2) ? ~0 : 0;
int a = SelectBit(3, 1, Mask);
代码片段 10:SelectBit 的使用为 SIMD 寄存器准备数据

现在,这可能看起来毫无意义,如果我们仍然有一个 ?: 运算符来创建掩码,并且比较的结果不是 true 或 false,而是所有位都设置或清除。但这不成问题,因为所有位都设置或清除实际上就是 Intel SSE 和 Intel AVX 的比较指令返回的结果。

当然,您可以调用函数并选择所需返回的结果,而不是只分配 3 或 1。这甚至可以在非向量化代码中带来性能提升,因为您可以避免分支,CPU 也不会遭受分支预测失败的痛苦,但您调用的函数越复杂,预测失败的可能性就越大。即使在向量化代码中,我们也会通过检查所有 SIMD 寄存器元素具有相同比较结果的特殊情况来避免执行不必要的长分支,如代码片段 11 所示。

int Mask = (i % 2) ? ~0 : 0;
int a = 0;
if(All(Mask))
	a = Function1();
else
if(None(Mask))
	a = Function3();
else
	a = BitSelect(Function3(), Function1(), Mask);
代码片段 11:展示了两个函数之间优化的无分支选择

这会检测所有元素都为“true”或所有元素都为“false”的特殊情况。这些情况在 SIMD 上与 x86 上运行相同,只有最后的“else”情况是执行流程会发散的地方,因此我们需要使用位选择。

如果 Function1 或 Function3 修改了任何数据,您需要将掩码传递下去,并显式地使用位选择来修改,就像我们在这里所做的一样。对于即插即用解决方案来说,这需要一些工作,但仍然可以生成大多数程序员可读的代码。

7. 复杂示例

让我们再次采用一些源代码并引入我们的 SIMD 类型。一个特别有趣的案例是距离字段的光线追踪。为此,我们将使用 Iñigo Quilez 的演示[2]中的场景,并征得他的许可,如图 1 所示。

图 1:来自 Iñigo Quilez 的光线投射演示的测试场景

“SIMD 线程”被放置在通常会添加线程的位置。每个线程处理一个像素,遍历世界直到碰到某个物体,然后进行一些着色处理,并将像素转换为 RGBA 并写入帧缓冲区。

场景遍历是以迭代方式完成的。每条光线需要不可预测的步数才能识别命中。例如,近处的墙壁在几步内就能达到,而某些光线可能达到最大追踪距离仍未击中任何东西。我们在代码片段 12 中的主循环处理这两种情况,使用我们在上一节讨论过的位选择方法。

DRealU LoopMask(RTrue);
for(; a < 128; a++)

{
      DRealF Dist             =     SceneDist(O.x, O.y, O.z, C);
      DRealU DistU            =     *reinterpret_cast<DRealU*>(&Dist) & DMask(LoopMask);
      Dist                    =     *reinterpret_cast<DRealF*>(&DistU);
      TotalDist               =     TotalDist + Dist;
      O                       +=    D * Dist;
      LoopMask                =     LoopMask && Dist > MinDist && TotalDist < MaxDist;
      if(DNone(LoopMask))
            break;
}
代码片段 12:使用 SIMD 类型的光线投射

LoopMask 变量通过 ~0 或 0 来识别光线是否处于活动状态,在这种情况下,我们已完成该光线。在循环结束时,我们测试是否没有光线仍然处于活动状态,在这种情况下,我们退出循环。

在上面的行中,我们评估光线的条件,即我们是否足够接近物体而可以称之为命中,或者光线是否已经超出了我们想要追踪的最大距离。我们将其与前一个结果进行逻辑 AND,因为光线可能已经在之前的迭代中终止了。

"SceneDist" 是我们追踪的评估函数 - 它为所有 SIMD 通道运行,并且是返回当前到最近物体距离的重量级函数。下一行将非活动光线的距离元素设置为 0,并为下一次迭代前进该距离。

原始的“SceneDist”包含了一些汇编器优化和材质处理,这些对于我们的测试来说并不需要,而这个函数被简化为我们拥有复杂示例所需的最小值。其中仍然包含一些 if 语句,这些语句的处理方式与之前完全相同。总的来说,“SceneDist”相当庞大且相当复杂,需要很长时间才能手动为每个 SIMD 平台重写一次。您可能需要一次性转换所有内容,而拼写错误可能会导致完全错误的结果。即使有效,您也只会理解少数几个函数,并且维护成本会更高。手动完成应该是最后的手段。相比之下,我们的更改相对较小。它易于修改,您可以扩展视觉效果,而无需担心再次优化它,并且您将是唯一一个理解代码的维护者,就像添加真实线程一样。

但我们付出了这些努力是为了看到结果,所以让我们在表 3 中查看计时。

表 3:包含循环展开类型的光线追踪性能,使用原始数据类型和 SIMD 数据类型

类型 帧率 (FPS) 加速比

x86

0.992 FPS

1.0 倍

SSE4

3.744 FPS

3.8 倍

SSE4 x2

3.282 FPS

3.3 倍

AVX2

6.960 FPS

7.0 倍

AVX2 x2

5.947 FPS

6.0 倍

 

您可以看到加速比并不是线性扩展的,这主要是因为分支。某些光线可能需要比其他光线多 10 倍的迭代次数。

8. 为什么不让编译器来做?

如今的编译器可以在一定程度上进行向量化,但生成代码的首要任务是提供正确的结果,因为您不会使用速度快 100 倍但有 1% 时间产生错误结果的二进制文件。我们做的一些假设,例如数据将对齐以进行 SIMD 处理,并且我们分配了足够的填充以避免覆盖连续的分配,这些超出了编译器的范围。您可以从 Intel 编译器获得关于它因无法保证的假设而跳过的所有机会的注解,您可以尝试重新组织代码并向编译器做出承诺,以便它生成向量化版本。但这需要您在每次修改代码时都进行的工作,并且在分支等更复杂的情况下,您只能猜测它会生成无分支的位选择还是串行化的代码。

编译器也没有您打算创建内容的内部知识。您知道线程是发散还是同质的,并实现分支或位选择的解决方案。您看到了攻击点,即最适合更改为 SIMD 的循环,而编译器只能猜测它会运行 10 次还是 100 万次。

依赖编译器可能在一处获益,在另一处带来痛苦。最好有一个您可以依赖的替代解决方案,就像您手动放置的线程入口一样。

9. 真实线程?

是的,真实线程很有用,SIMD 线程并非替代品——两者是正交的。SIMD 线程的运行仍然不如真实线程简单,但您也会遇到更少的同步和罕见 bug 的麻烦。真正的好处是 Intel 出售的每个核心都可以运行您的 SIMD 线程版本,其中包含所有“线程”。双核 CPU 将运行快 4 或 8 倍,就像您的四路 15 核 Haswell-EP 服务器一样。我们在基准测试中结合线程的一些结果总结在表 4 到表 7 中。1

表 4:在 Intel® Core™ i7 4770K 上同时使用 SIMD 和线程的 MD5 性能

线程 类型 时间 加速比

1T

x86 整数

311.704 秒

1.00 倍

8T

x86 整数

47.032 秒

6.63 倍

1T

SSE4

90.601 秒

3.44 倍

8T

SSE4

14.965 秒

20.83 倍

1T

SSE4 x2

62.225 秒

5.01 倍

8T

SSE4 x2

12.203 秒

25.54 倍

1T

AVX2

42.071 秒

7.41 倍

8T

AVX2

6.474 秒

48.15 倍

1T

AVX2 x2

29.612 秒

10.53 倍

8T

AVX2 x2

5.616 秒

55.50 倍

 

表 5:在 Intel® Core™ i7 4770K 上同时使用 SIMD 和线程的光线追踪性能

线程 类型 帧率 (FPS) 加速比

1T

x86 整数

1.202 FPS

1.00 倍

8T

x86 整数

6.019 FPS

5.01 倍

1T

SSE4

4.674 FPS

3.89 倍

8T

SSE4

23.298 FPS

19.38 倍

1T

SSE4 x2

4.053 FPS

3.37 倍

8T

SSE4 x2

20.537 FPS

17.09 倍

1T

AVX2

8.646 FPS

4.70 倍

8T

AVX2

42.444 FPS

35.31 倍

1T

AVX2 x2

7.291 FPS

6.07 倍

8T

AVX2 x2

36.776 FPS

30.60 倍

 

表 6:在 Intel® Core™ i7 5960X 上同时使用 SIMD 和线程的 MD5 性能

线程 类型 时间 加速比

1T

x86 整数

379.389 秒

1.00 倍

16T

x86 整数

28.499 秒

13.34 倍

1T

SSE4

108.108 秒

3.51 倍

16T

SSE4

9.194 秒

41.26 倍

1T

SSE4 x2

75.694 秒

5.01 倍

16T

SSE4 x2

7.381 秒

51.40 倍

1T

AVX2

51.490 秒

3.37 倍

16T

AVX2

3.965 秒

95.68 倍

1T

AVX2 x2

36.015 秒

10.53 倍

16T

AVX2 x2

3.387 秒

112.01 倍

 

表 7:在 Intel® Core™ i7 5960X 上同时使用 SIMD 和线程的光线追踪性能

线程 类型 帧率 (FPS) 加速比

1T

x86 整数

0.992 FPS

1.00 倍

16T

x86 整数

6.813 FPS

6.87 倍

1T

SSE4

3.744 FPS

3.774 倍

16T

SSE4

37.927 FPS

38.23 倍

1T

SSE4 x2

3.282 FPS

3.31 倍

16T

SSE4 x2

33.770 FPS

34.04 倍

1T

AVX2

6.960 FPS

7.02 倍

16T

AVX2

70.545 FPS

71.11 倍

1T

AVX2 x2

5.947 FPS

6.00 倍

16T

AVX2 x2

59.252 FPS

59.76 倍

 

1性能测试中使用的软件和工作负载可能仅针对 Intel 微处理器进行了性能优化。性能测试(如 SYSmark* 和 MobileMark*)是通过使用特定的计算机系统、组件、软件、操作和功能来衡量的。任何这些因素的更改都可能导致结果有所不同。您应该参考其他信息和性能测试,以帮助您全面评估您打算购买的产品,包括该产品与其他产品结合使用时的性能。有关更多信息,请访问 http://www.intel.com/performance

正如您所见,线程结果因 CPU 而异,SIMD 线程结果的扩展相似。但令人震惊的是,结合这两种想法可以达到两位数的加速因子。双核 CPU 加速 8 倍是有意义的,而昂贵的服务器硬件再增加 8 倍的加速也是有意义的。

加入我,给您的代码 SIMD 化吧!

关于作者

Michael Kopietz 是 Crytek 研发部的渲染架构师,领导着一个开发 CryEngine(R) 渲染的工程师团队,并指导学生完成他们的论文。他从事过跨平台渲染架构、软件渲染以及高性能、可重用代码至上的高响应服务器等工作。在此之前,他曾从事舰船战斗和足球模拟游戏的渲染工作。他最初从事早期家用游戏机的汇编器编程,至今仍希望让每一周期都发挥作用。

代码许可

本文中的所有代码 © 2014 Crytek GmbH,并在 https://software.intel.com/en-us/articles/intel-sample-source-code-license-agreement 许可下发布。保留所有权利。

参考文献

[1] Intel® Xeon Phi™ 协处理器上的内存管理以获得最佳性能:对齐和预取 https://software.intel.com/en-us/articles/memory-management-for-optimal-performance-on-intel-xeon-phi-coprocessor-alignment-and

[2] 使用两个三角形渲染世界,作者 Iñigo Quilez http://www.iquilezles.org/www/material/nvscene2008/nvscene2008.htm

© . All rights reserved.