在现代计算环境中,多核处理器的普及使得并行计算变得尤为重要。本文将探讨如何利用多线程技术来解决特定问题,并通过实际案例分析,展示不同技术在解决同一问题时的性能表现。
面临的问题是,对于1到1000000之间的每一个数字n,应用一个函数,直到这个数字变为1,同时计算函数被应用的次数。由于硬件限制,将算法的执行范围限制在1到100000之间。程序不会从键盘读取任何输入,并将打印出结果和计算结果所需的执行时间(以毫秒为单位)。测试机器的配置如下:AMD Athlon 2 P340 Dual Core 2.20 GHz,4 GB RAM,ATI Mobility Radeon 5470图形适配器,Windows 7 Ultimate x64。实现语言为C++(Visual Studio 2010)。
在深入探讨之前,建议读者先阅读之前的文章《代码优化教程 - 第一部分》和《代码优化教程 - 第二部分》。此外,对WinAPI的了解也是推荐的,尽管不是必需的。
首先,将使用SSE2内在函数实现算法(main_v10.cpp)。由于CPU不支持SSE4(增加了对打包等值比较指令的支持),被迫减少了处理的数字范围(最大100000)。
// C++代码
while((comparedValue.m128i_i64[0] != 0) || (comparedValue.m128i_i64[1] != 0)) {
// 计算每个数字的最后一个比特
oddBit = _mm_and_si128(numbersToProcess, oneValue);
// 计算数字 / 2
tempNumber = _mm_srli_epi32(numbersToProcess, 1);
// 计算位掩码
mask = _mm_cmpeq_epi32(oddBit, oneValue);
// 将奇数除以2后加1
tempNumber = _mm_add_epi32(tempNumber, oddBit);
// 屏蔽偶数
numbersToProcess = _mm_and_si128(numbersToProcess, mask);
// 增加当前周期数
currentCycles = _mm_add_epi32(currentCycles, oneValue);
// 更新要处理的数字
numbersToProcess = _mm_add_epi32(tempNumber, numbersToProcess);
// 为奇数增加当前周期计数
currentCycles = _mm_add_epi32(currentCycles, oddBit);
// 检查数字是否等于1
comparedValue = _mm_cmpeq_epi32(numbersToProcess, oneValue);
// 检查数字是否小于1
mask = _mm_cmplt_epi32(numbersToProcess, oneValue);
// 将操作结果组合
comparedValue = _mm_or_si128(comparedValue, mask);
// 反转或操作的结果
comparedValue = _mm_xor_si128(comparedValue, ffValue);
// 将小于或等于1的数字的位清零
numbersToProcess = _mm_and_si128(numbersToProcess, comparedValue);
}
退出while循环后,将计算最大周期数。这种实现的执行时间为:23.97毫秒。
在DirectCompute实现(shader.hlsl/main_v11.cpp)中,将算法运行在GPU上。ComputeShader通过使用线程ID作为需要处理的数字,并对其应用以下算法:
// C++代码
while(nr > 1) {
nr2 = nr >> 1;
if(nr & 0x1) {
nr2 = nr2 + nr + 1;
count++;
}
count++;
nr = nr2;
}
在运行在CPU上的部分,创建了Direct3D设备,设置了输出缓冲区和非结构化访问视图,然后调用Dispatch函数处理100000个数字。ComputeShader完成后,将输出复制到CPU可以读取的缓冲区,并计算最大值。这个版本的执行时间为:185.97毫秒。
OpenCL版本的代码由于无法获得GPU设备(认为这是驱动程序问题,因为可以在计算机上运行DirectCompute程序),所以遇到了困难。在设置部分,程序获取设备,创建命令队列,然后创建输入和输出缓冲区,然后加载程序并编译它,最后获取内核函数。