61、计算机架构与C++并行编程全解析

计算机架构与C++并行编程全解析

1. 计算机架构基础

在计算机系统中,虚拟内存的使用十分关键。程序使用逻辑地址而非物理地址来访问内存,逻辑地址由硬件与操作系统协作映射到物理内存。虚拟内存页(地址范围从页面大小的倍数开始,页面大小为2的幂)通过结合硬件缓冲区(转换后备缓冲区 - TLBs)和操作系统管理的转换数据结构,改变地址的最高有效位来映射到物理内存。

在分配内存时,除了虚拟地址转换的开销,还需要考虑虚拟内存映射到哪个插槽。在LINUX中,分配内存的线程默认会在其本地插槽上分配内存。当然,也有其他的放置策略,例如采用循环映射,即数组的第i个块映射到插槽i mod P,其中P是插槽的数量。

2. 互连网络

并行计算机的处理器通过互连网络连接。当处理器数量较少时,使用全点对点链路的完整网络是可行的;而当处理器数量较多时,则需要使用稀疏网络。常见的互连网络有以下几种:
| 网络类型 | 特点 |
| ---- | ---- |
| 二维和三维网格 | 可以任意扩展,且线长有界 |
| 环形网络 | 每个方向都有环形互连,物理布局连接短 |
| 超立方体(log p维网格) | 一种特殊的网络结构 |
| 胖树 | 一种多级网络,可近似完整互连网络 |

在稀疏互连网络中,任意点对点通信需要通过网络中的路径发送消息来实现。部分路径可能会共享同一根电线,从而导致争用。精心设计的路由算法可以减少这种争用。例如,在具有p个节点的超立方体中,两个节点之间路径的最大长度为Θ(log p),因此最小传输时间为Θ(log p)。存在一些路由算法可以保证p条具有不同源和不同目的地的消息在O(log p)时间内传输完成。

graph LR
    A[处理器1] --> B(互连网络)
    C[处理器2] --> B
    D[处理器3] --> B
    B --> E[处理器4]
    B --> F[处理器5]
    B --> G[处理器6]
3. CPU性能分析

代码优化需要进行性能分析,原因主要有两点:一是算法分析通常只关注渐近情况,忽略了常数因子;二是我们对顺序和并行计算机的模型只是对实际硬件的近似。现代处理器配备了专用的硬件性能监控单元(PMUs)来支持性能分析。PMUs可以统计性能事件(如CPU周期、缓存未命中、分支预测错误等),并将这些事件映射到特定的指令。

常见的性能分析工具包括:
- Linux perf :开源的性能分析器,对常见CPU架构有良好的支持。
- Intel VTune Amplifier :针对Intel CPU架构的高级分析器。

为了减少对程序执行的副作用,大多数分析器采用对硬件性能事件进行统计采样的方法。如果不需要将事件映射到指令,PMUs可以在计数模式下使用,例如在程序执行前后读取以统计事件总数。

对于短时间操作的性能分析,传统的采样或计数性能事件方法由于相对开销较大而不太适用。最近,Intel引入了处理器跟踪(PT)技术,可用于收集完整的指令执行跟踪和时间戳,相关分析功能在Intel VTune Amplifier和Linux perf中可用。

4. 编译器

在编译共享内存程序示例时,通常使用GNU C++编译器(g++)版本4.7.2。具体的编译器选项可以在github.com下的basic-toolbox-sample-code/basic-toolbox-sample-code/的Makefiles中找到。

5. C++中的并行支持

C++11标准通过新的语言构造为共享内存系统中的多线程并行提供了原生支持。这些新构造隐藏了线程管理的实现细节,提供了基本锁、通用原子操作和异步任务支持。C++14标准增加了一种用于共享访问的新锁类型。

5.1 “Hello World” C++11多线程程序示例

以下是一个简单的多线程程序示例,展示了线程的基本管理:

#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
using namespace std;
mutex m;
void worker(int iPE) {
    m.lock();
    cout << "Hello from thread "<< iPE << endl;
    m.unlock();
}
int main() {
    vector<thread> threads(thread::hardware_concurrency());
    int i = 0;
    for(auto & t: threads) t = thread(worker, i++);
    for(auto & t: threads) t.join();
    return 0;
}
//SPDX−License−Identifier: BSD−3−Clause; Copyright(c) 2018 Intel Corporation

在这个示例中,程序创建了硬件支持的最大数量的线程,每个线程执行一个用户定义的工作函数。由于 cout 对象是共享资源,为了避免字符混乱,使用了互斥锁(mutex)进行保护。主线程通过调用 join 函数等待所有工作线程完成。

由于创建和加入线程涉及操作系统调用,开销较大,因此对于小型工作项,不建议为每个工作项单独创建一个C++线程。对于健壮的多线程应用程序,建议一次性创建所有所需的线程(线程池),并使用负载均衡方法将工作分配给它们。

5.2 锁机制

C++11提供了多种锁类,包括:
- mutex :互斥锁,用于基本的互斥访问。
- recursive_timed_mutex :支持超时的递归锁。
- recursive_mutex :可多次获取的递归锁。
- shared_timed_mutex :可区分读写操作的锁。

为了减少锁使用时的错误,C++11还提供了一些辅助类,如 lock_guard ,它在构造函数中获取锁,并在析构函数中自动释放锁。 lock 函数可以同时锁定多个互斥锁,避免死锁的发生。

5.3 异步操作

promise future 类提供了比基本锁机制更高级的线程交互机制。一个线程可以产生一个值,其他线程可以等待该值可用。一种有用的变体是异步函数调用,它返回一个 future 对象,线程可以在后续等待函数调用完成。

5.4 原子操作

C++11为共享内存并行计算机提供了原子操作的抽象,包括原子加载和存储、交换、强和弱比较并交换(相当于CAS)、取和加、取和减、取和或、取和与、取和异或等操作,适用于C++内置的8位、16位、32位和64位整数和字符类型,还有原子布尔类型。

原子操作有不同的内存顺序保证,默认和最安全的内存顺序类型是 memory_order_seq_cst (顺序一致性),它提供了许多保证,如读写操作不能重新排序,所有线程对同一原子变量的修改顺序一致等。为了强制在任意操作(包括非原子操作)之间实现所需的内存顺序,C++提供了 atomic_thread_fence 函数,而 atomic_signal_fence 只能防止编译器对操作进行重新排序。

需要注意的是,C++11标准中的原子操作和类型虽然丰富,但只是厂商特定处理器功能的最低共同点。例如,x86架构支持的128位CAS操作并未包含在C++标准中。

6. 有用的工具

除了C++标准提供的并行支持,还有一些外部工具可用于并行编程:
- OpenMP :一种编译器扩展,通过在顺序程序中添加编译器指令来帮助编译器进行并行化。支持单程序多数据(SPMD)编程、局部和全局变量以及并行循环,可直接支持锁定,也可与其他库一起使用。
- 任务并行编程 :OpenMP从3.0版本开始支持任务并行编程,但早期实现性能不佳。Intel的Cilk Plus和Threading Building Blocks(Intel TBB)使用工作窃取负载均衡器,性能更好,但基于任务并行编程的算法有时在跨处理器芯片时扩展性不佳。
- 软件库 :许多C++软件库利用了并行性,如线性代数库和并行实现的C++标准模板库(STL)。MC-STL是STL的一个很好的并行化实现,是GNU C++发行版的一部分。

7. 内存管理

C++的内存分配函数( new )会调用底层操作系统的分配器(如Linux上的 malloc )。标准内存分配器是通用的,并非针对最大可扩展性进行优化。通常,小尺寸的内存分配从受进程独占锁保护的进程本地堆中获取,这可能导致可扩展性瓶颈;大尺寸的内存分配直接向操作系统请求,也涉及操作系统内存结构的锁定。此外,出于安全原因,所有分配的内存都需要由操作系统初始化。

为了提高性能,可以采用以下方法:
- 使用用户空间内存池 :Intel Threading Building Blocks和Boost库提供了具有标准分配和释放接口的用户空间内存池。
- 替换标准分配器 :Google的Thread Caching Malloc和Intel TBB可扩展内存分配器提供了按线程的堆,避免了全局锁,并自动缓存内存以供重用。
- 内存对齐分配 :某些操作需要特定的内存对齐,常见操作系统提供了支持对齐的自定义分配器,Intel TBB也提供了可返回缓存行对齐内存的分配器。
- 控制内存放置 :大多数操作系统提供了库和接口,允许应用程序控制内存放置在特定的插槽上,如Linux上的 libnuma 和Windows上的 VirtualAllocExNuma

8. 线程调度

默认情况下,用户线程的执行时间和执行的硬件线程或核心是不确定的,操作系统可以根据优化目标(通常是启发式方法)任意调度和迁移线程。线程迁移可能会对性能产生负面影响,例如线程无法再使用最近访问的缓存行,访问不同插槽上分配的内存时延迟增加等。

为了防止线程迁移,开发者可以使用以下方法将用户线程固定到一组硬件线程上:
- Linux :使用 pthread_setaffinity_np 调用。
- Windows :使用 SetThreadGroupAffinity

在Linux上,线程固定功能还可以用于控制非统一内存访问(NUMA)分配。如果线程访问尚未分配到物理内存的虚拟内存块(延迟内存分配),Linux的默认策略是尝试在本地插槽上分配物理内存。

计算机架构与C++并行编程全解析

9. 并行编程中的关键要点总结

在并行编程的实践中,有几个关键要点需要开发者特别关注,以下是对这些要点的总结表格:
| 要点 | 说明 |
| ---- | ---- |
| 线程管理 | 避免为每个小工作项单独创建线程,建议使用线程池并结合负载均衡方法。创建和销毁线程的开销较大,线程池可以提高效率。 |
| 锁的使用 | 选择合适的锁类型,如mutex、recursive_timed_mutex等,并利用辅助类如lock_guard和lock函数来减少错误和避免死锁。 |
| 异步操作 | 利用promise和future类实现线程间的交互,通过异步函数调用返回future对象,方便后续等待函数完成。 |
| 原子操作 | 理解不同的内存顺序保证,优先使用默认的memory_order_seq_cst,同时注意C++标准中原子操作的局限性。 |
| 工具选择 | 根据具体需求选择合适的并行编程工具,如OpenMP、Cilk Plus、Intel TBB等,以及并行化的软件库。 |
| 内存管理 | 优化内存分配,采用用户空间内存池、替换标准分配器、进行内存对齐分配和控制内存放置等方法提高性能。 |
| 线程调度 | 必要时固定线程到特定的硬件线程,防止线程迁移带来的性能损失,在Linux和Windows上有相应的调用函数。 |

10. 并行编程的实践流程

为了更好地进行并行编程开发,我们可以总结出一个实践流程,以下是使用mermaid绘制的流程图:

graph LR
    A[需求分析] --> B[算法设计]
    B --> C[选择工具和库]
    C --> D[编写代码]
    D --> E[性能分析]
    E --> F{性能达标?}
    F -- 是 --> G[部署上线]
    F -- 否 --> H[优化代码]
    H --> E

具体的操作步骤如下:
1. 需求分析 :明确并行编程的目标和需求,确定要解决的问题和性能指标。
2. 算法设计 :根据需求设计合适的并行算法,考虑数据划分、任务分配和同步机制等。
3. 选择工具和库 :根据算法和性能要求,选择合适的并行编程工具和软件库,如OpenMP、Intel TBB等。
4. 编写代码 :使用选定的工具和库编写并行代码,注意线程管理、锁的使用、异步操作和原子操作等关键要点。
5. 性能分析 :使用性能分析工具(如Linux perf、Intel VTune Amplifier)对代码进行分析,找出性能瓶颈。
6. 判断性能是否达标 :根据性能指标判断代码是否满足要求。
7. 优化代码 :如果性能不达标,根据性能分析结果对代码进行优化,如调整算法、改进内存管理或线程调度等。
8. 部署上线 :当性能达标后,将代码部署到生产环境中。

11. 并行编程的常见错误及解决方法

在并行编程过程中,开发者可能会遇到一些常见的错误,以下是对这些错误及解决方法的总结:
| 常见错误 | 错误原因 | 解决方法 |
| ---- | ---- | ---- |
| 数据竞争 | 多个线程同时访问和修改共享数据,导致数据不一致。 | 使用锁机制(如mutex)或原子操作来保护共享数据。 |
| 死锁 | 线程之间互相等待对方释放锁,导致程序无法继续执行。 | 使用lock函数同时锁定多个互斥锁,避免死锁;合理设计锁的获取和释放顺序。 |
| 性能瓶颈 | 线程创建和销毁开销大、内存分配不合理、线程迁移等原因导致性能不佳。 | 使用线程池、优化内存管理、固定线程到特定硬件线程等方法提高性能。 |
| 内存泄漏 | 动态分配的内存没有正确释放,导致内存占用不断增加。 | 确保在不再使用内存时及时释放,使用智能指针等工具辅助内存管理。 |

12. 总结与展望

并行编程在现代计算机系统中具有重要的地位,可以充分利用多核处理器的性能,提高程序的执行效率。通过了解计算机架构的相关知识,如虚拟内存、互连网络、CPU性能分析等,以及掌握C++中的并行编程支持和相关工具,开发者可以更好地进行并行编程开发。

在实际应用中,需要根据具体的需求和场景选择合适的算法、工具和技术,同时注意避免常见的错误,不断优化代码以提高性能。随着计算机技术的不断发展,并行编程的应用场景将越来越广泛,未来可能会出现更多高效的并行编程工具和技术,为开发者带来更多的便利和挑战。

希望本文能够帮助读者更好地理解并行编程的相关知识和技术,在实际开发中取得更好的效果。

内容概要:本文介绍了一个基于Matlab的综合能源系统优化调度仿真资源,重点实现了含光热电站、有机朗肯循环(ORC)和电含光热电站、有机有机朗肯循环、P2G的综合能源优化调度(Matlab代码实现)转气(P2G)技术的冷、热、电多能互补系统的优化调度模型。该模型充分考虑多种能源形式的协同转换利用,通过Matlab代码构建系统架构、设定约束条件并求解优化目标,旨在提升综合能源系统的运行效率经济性,同时兼顾灵活性供需不确定性下的储能优化配置问题。文中还提到了相关仿真技术支持,如YALMIP工具包的应用,适用于复杂能源系统的建模求解。; 适合人群:具备一定Matlab编程基础和能源系统背景知识的科研人员、研究生及工程技术人员,尤其适合从事综合能源系统、可再生能源利用、电力系统优化等方向的研究者。; 使用场景及目标:①研究含光热、ORC和P2G的多能系统协调调度机制;②开展考虑不确定性的储能优化配置经济调度仿真;③学习Matlab在能源系统优化中的建模求解方法,复现高水平论文(如EI期刊)中的算法案例。; 阅读建议:建议读者结合文档提供的网盘资源,下载完整代码和案例文件,按照目录顺序逐步学习,重点关注模型构建逻辑、约束设置求解器调用方式,并通过修改参数进行仿真实验,加深对综合能源系统优化调度的理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值