代码优化教程 - 第二部分






4.84/5 (15投票s)
初学者优化教程。
引言
在代码优化教程的第二部分,我们将探讨可应用于使用线程的算法的优化技术。
我将选择与上一篇文章 相同的问题,这次利用多核处理器
对于1到1000000之间的每个数字n,应用以下函数
直到数字变为1,计算应用函数的次数。
此算法将对1到1000000之间的所有数字执行。不会从键盘读取任何输入 数字,程序将打印结果, 以及计算结果所需的执行时间(以毫秒为单位)。
测试 机器将是一台配备以下规格的笔记本电脑:AMD Athlon 2 P340 双核 2.20 GHz,4 GB RAM,Windows 7 Ultimate x64。
实现所用语言:C# 和 C++(Visual Studio 2010)。
在此文章中,不再考虑程序的 Debug 版本。
必备组件
文章: 代码优化教程 - 第一部分。
建议具备 WinAPI 知识,但非强制。实现
我们有 NumCores 个处理器核心。
我将使用一个队列来存储要处理的数字。此外,为每个可用的处理器核心创建一个工作线程。所有工作线程(仅读取)都操作此共享队列。每个工作线程从队列中提取一个数字并计算所需值。数字由主线程写入队列,主线程是唯一写入队列的线程。工作线程运行直到所有数字都已处理完毕。
对于每个数字的计算,在C++中我将使用上一篇文章中提出的算法,在C#中我将使用anlarke提出的算法。
for (iCurrentCycleCount = 1; iNumberToTest != 1; ++iCurrentCycleCount)
{
if ((iNumberToTest & 0x1) == 0x01)
{
iNumberToTest += (iNumberToTest >> 1) + 1;
++iCurrentCycleCount;
}
else
iNumberToTest >>= 1;
}
这是结果(main_v6.cpp/Program_v6.cs)
C++ | C# |
3758.64 | 563.64 |
正如所观察到的,C++版本的执行时间非常长。
这次是由于
1) 我只使用一个队列在 NumCore + 1 个线程之间共享
2) 我对队列进行读写操作时进行了大量的锁定。
对于第一点,为了部分消除工作线程之间的并发问题,我将为每个工作线程使用一个队列,并发问题将仍然存在于主线程和每个工作线程之间。因此,为了最小化线程等待另一个线程释放其锁的时间,我将队列分成 NumCore 个队列。
这是结果(main_v7.cpp/Program_v7.cs)
C++ | C# |
1855.36 | 1418.08 |
C++在速度上有显著提升。我不知道为什么C#版本会慢这么多。
关于第二个方面,即锁操作的数量,我将选择一次写入250个数字到队列中,而不是一次写入1个数字(数字由测试确定)。
这是结果(main_v8.cpp/Program_v8.cpp)
C++ | C# |
925.89 | 393.71 |
C++和C#在执行时间上都有显著的改进。
C++的执行时间比C#慢,因为在C++中同步我使用互斥锁(在内核空间实现),而在C#中我使用lock关键字,它使用临界区(在用户空间实现)来实现。
我提供了一个使用临界区的C++版本,在文件main_v8a.cpp中。该版本的执行时间为484.95毫秒。
还有一种优化可以进行:在这种情况下,因为线程遵循生产者-消费者模式,并且我们确切知道程序将处理的数据,我们可以完全消除队列的锁定,通过在启动线程之前将数据加载到队列中。
这是结果(main_v9.cpp/Program_v9.cpp)
C++ | C# |
304.23 | 291.91 |
C++的速度有了显著提升,C#版本也有了改进。
关注点
本文结论
1. 尽量不要在线程之间共享数据。
2. 尽量减少锁操作的数量,因为它们很昂贵,批量处理数据,而不是一次处理一个。
3. 如果您不需要进程间同步,请使用用户空间同步对象。
4. 如果您的线程行为符合生产者-消费者模式,并且生产者线程需要提供给消费者的数据在启动消费者线程之前是已知的,那么您可以消除锁定。
下次,我将讨论使用SSE、DirectCompute和OpenCL优化程序。
历史
- 2012.06.21 - 首次发布。
- 2012.06.22 - 重新上传代码。