拓冰建站拓冰建站
首页 / 资讯中心 / 正文

面向高通量微体系结构评估的Pthread并行MicroBenchmark研究

简介《面向处理器微体系结构评估的高通量MicroBenchmark研究》是一篇发表于《计算机研究与发展》的学术论文面向计算机体系结构方向的研究生、处理器设计与性能评测人员聚焦高通量应用场景下微体系结构评估缺乏合适基准程序的问题。文中提出面向高通量应用的基准测试集HTC-MicroBench基于PThread并行模型设计实现从并发度、数据耦合、缓存效率等维度展开评测并梳理任务粒度、并行性模型、内存访问模式、资源竞争与实时性等设计要点可为PPA优化及新指令集、缓存策略评估提供参考。压缩包内含1个PDF文件约1.84MB开篇为中英文摘要与关键词便于快速判断论文价值正文按章节呈现分类方法、并行化模型与实验数据适合按需精读或作为体系结构课程的参考文献。目前已有70人学习下载。1. 当 SPEC CPU 跑不动一个日志分词统计任务时问题出在哪拿 SPEC CPU2006 去评一颗面向互联网服务的多核处理器分数往往很漂亮但真把日志分词统计、倒排索引重建这类活儿压上去吞吐却上不去。这不是编译器或系统调优的问题而是基准程序和被评对象之间对不上号——SPEC 那套负载追求的是单个应用跑得多快工作集规整、访存局部性好、实时性要求低高通量应用几乎处处相反任务细碎、并发度高、数据耦合弱、时间约束硬。用前者去评估后者的微体系结构设计测出来的数字参考价值有限。《面向处理器微体系结构评估的高通量 MicroBenchmark 研究》补的就是这个缺口。它先给高通量应用建立一套基于负载特征的分类模型据此选出代表性 Workload再用 Pthread 并行化模型实现测试集 HTC-MicroBench最后从作业并发性、作业间的耦合性和 Cache 使用效率三个维度评估 TILE-Gx 与 Xeon 的并行加速能力。下面按这套思路拆一遍包括分类怎么分、并行模型怎么写、指标怎么采、拐点怎么读。2. 高通量 Workload 分类模型与代表性测试集选型2.1 高通量负载与传统高性能负载差在哪要设计基准测试第一步是承认这两类负载不是同一种东西。面向科学计算的负载以 LINPACK 这类程序为代表数据结构规则、局部性好属于数据密集加计算密集型目标是缩短单个应用的墙钟时间面向网络服务的高通量负载则是请求密集加数据密集任务之间基本不共享状态追求的是单位时间内处理完的并发任务数。两者的差异落到微体系结构上关注的部件都不一样了前者盯浮点单元和向量宽度后者盯线程调度、共享缓存、片上互连和一致性协议的开销。对比维度高性能负载科学计算高通量负载网络服务场景科学计算网络服务目标高速完成单个应用单位时间处理更多任务访存局部性好差数据结构规则不规则实时性需求低高密集类型数据、计算密集数据、请求密集表格里最关键的一行是局部性。局部性一差Cache 命中率就成了高通量处理器设计里最容易出问题的地方也正是后面评估指标要重点量化的一环。2.2 按需求划成三类数据处理、数据服务、实时交互对表 2 那批应用做特征归并后高通量应用的需求可以收敛成三句话单位时间内处理尽量大的数据量、单位时间内响应尽量多的请求、在硬时间约束下同时撑住尽量多的在线用户。这三条需求各自对应一类 Workload。数据处理类以基础算法为主字符统计Wordcount、Tera 级排序Terasort、聚类K-means、搜索匹配Grep都是大数据分析里的地基调用量大、变体多覆盖的是批量吃数据的场景。数据服务类的典型是解析字符串Query Parser、数据库操作Database Query、计算词频Term Weight和计算网页权重Document Weight它们共同点是单次请求计算量小、但请求密度极高考验的是调度和延迟。实时交互类包括源数据简单处理SDU Receive、数据包分析转储控制MACD Schedule、用户实例查询Entity Query和报文协议处理Segment这类负载对时延抖动最敏感。三类划分不是学术分类癖而是直接决定测试集里每个 Workload 应该配多大的工作集、开多少线程、跑多长测量窗口。把三类混在一起测得到的曲线是没法定性的。2.3 选型的两条原则与登记方式选 Workload 只有两条硬标准一是每类里挑一个包含至少一个应用领域的代表因为同类的 Workload 在应用特征和性能指标上高度相似二是挑使用量大的使用量越大测出来的结果对真实部署越有解释力。按这两条HTC-MicroBench 收进了 13 个 Workload。实际落地时我一般给每个 Workload 建一份元数据登记而不是把参数写死在 main 函数里。这样做的好处是扫线程数、扫工作集的时候不用重新编译# workloads.yaml —— HTC-MicroBench 负载登记表节选 - name: wordcount class: data_processing # 数据处理类 domain: [big_data_analysis] default_threads: 8 # 默认线程数扫描时可覆盖 working_set_mb: 256 # 工作集大小用于扫 Cache 边界 input: /data/text/part-00000 metric: [throughput, llc_miss_rate] - name: term_weight class: data_service # 数据服务类 domain: [web_search, e_commerce] default_threads: 16 working_set_mb: 64 input: /data/index/terms.bin metric: [qps, cache_miss_rate] - name: segment class: real_time_interactive # 实时交互类 domain: [rnc, stream_media] default_threads: 8 working_set_mb: 32 input: /data/pkt/trace.bin metric: [throughput, tail_latency_p99]class字段决定这个负载挂到哪条评估线上working_set_mb是后面扫 Cache 边界的关键旋钮metric声明要采哪些指标——采什么指标在开跑之前就要定下来事后补采只能重跑。定位工作时集时建议先按 LLC 容量的一半设一版再按 1 倍、2 倍各设一版三档就能看出命中率的陡降点。3. 基于 Pthread 的作业处理节点并行化模型与实现3.1 为什么选 Pthread 而不是 OpenMP 或 MPI高通量应用的并行粒度小、任务数多、线程间几乎不共享写数据这三条决定了并行框架的选型。MPI 走进程间消息发起一次通信的开销比作业本身还大OpenMP 适合循环级并行但它的隐式同步和线程池调度不透明测线程调度效率的时候反而成了干扰项。Pthread 的优势在于显式线程怎么建、什么时候同步、绑到哪个核全在代码里看得见微体系结构评估要的正是这种可观测性。核心思想是把一个处理节点抽象成作业队列加工作线程组。作业预先装进全局队列工作线程用原子游标争抢取到就干干完再取。线程之间没有锁竞争也不做数据分区这种取件式分配天然就是负载均衡的——某个线程慢一拍别的线程自然会多取几个作业。3.2 单生产者多消费者的骨架代码/* htc_run.c —— 高通量作业处理节点的并行化骨架 */ #include pthread.h #include stdatomic.h #include stdio.h #include stdlib.h #include string.h #define MAX_JOBS 4096 typedef struct { int id; const char *buf; /* 预加载的作业输入测量窗口内不碰文件系统 */ size_t len; } job_t; static job_t g_jobs[MAX_JOBS]; static atomic_int g_cursor; /* 全局作业游标取件式负载均衡 */ static int g_njobs, g_nthreads; static pthread_barrier_t g_start, g_end; /* 统一起跑与统一收尾 */ static inline void do_work(const job_t *j, unsigned long *acc) { /* 占位内核实际替换为 wordcount / terasort / grep 等算法 */ unsigned long h 0; for (size_t i 0; i j-len; i) h h * 131u (unsigned char)j-buf[i]; *acc h; } static void *worker(void *arg) { unsigned long *acc (unsigned long *)arg; pthread_barrier_wait(g_start); /* 所有线程就绪后同时开跑 */ int idx; while ((idx atomic_fetch_add(g_cursor, 1)) g_njobs) do_work(g_jobs[idx], acc); /* 无锁取作业 */ pthread_barrier_wait(g_end); /* 收拢测量窗口 */ return NULL; } int main(int argc, char **argv) { if (argc ! 3) { fprintf(stderr, usage: %s njobs nthreads\n, argv[0]); return 1; } g_njobs atoi(argv[1]); g_nthreads atoi(argv[2]); if (g_njobs MAX_JOBS) g_njobs MAX_JOBS; /* 输入一次性预加载避免 I/O 混进并发度指标 */ for (int i 0; i g_njobs; i) { g_jobs[i].id i; g_jobs[i].len 64 * 1024; g_jobs[i].buf malloc(g_jobs[i].len); memset((void *)g_jobs[i].buf, i 0xff, g_jobs[i].len); } atomic_store(g_cursor, 0); pthread_barrier_init(g_start, NULL, g_nthreads); pthread_barrier_init(g_end, NULL, g_nthreads); pthread_t *tids calloc(g_nthreads, sizeof(*tids)); unsigned long *accs calloc(g_nthreads, sizeof(*accs)); for (int i 0; i g_nthreads; i) pthread_create(tids[i], NULL, worker, accs[i]); for (int i 0; i g_nthreads; i) pthread_join(tids[i], NULL); unsigned long total 0; for (int i 0; i g_nthreads; i) total accs[i]; printf(jobs%d threads%d checksum%lu\n, g_njobs, g_nthreads, total); return 0; }编译运行gcc -O2 -pthread htc_run.c -o htc_run ./htc_run 4096 8几个设计点值得单独说。atomic_fetch_add取代互斥锁是因为作业粒度已经很小一把锁就能把并行度吃干净checksum累加不是为了算对错而是阻止编译器把整个循环优化掉同时给多线程结果一个可比的收敛值两道pthread_barrier把测量窗口卡死在全部就绪到全部干完之间避免线程创建和销毁的时间混进吞吐计算。njobs决定单次测量窗口的长度太短会被启动抖动主导一般取线程数的几百倍nthreads是后面扫可扩展性曲线的主变量。3.3 绑核与参数配置不绑核的话Linux 调度器会把线程在物理核之间搬来搬去Cache 里的数据跟着失效测出来的命中率没有意义。/* 把当前线程绑到第 core 号逻辑核降低迁移噪声 */ cpu_set_t set; CPU_ZERO(set); CPU_SET(core, set); if (pthread_setaffinity_np(pthread_self(), sizeof(set), set) ! 0) perror(pthread_setaffinity_np);参数建议取值影响njobs线程数 × 200 以上窗口太短会被启动开销主导nthreads1 到 2 倍物理核数超过核数用于观察调度开销作业大小32KB 到 256KB对齐 L1/L2 容量控制局部性绑核策略连续物理核优先减少 NUMA 远端访存同步方式barrier不用 sleep保证测量窗口对齐绑核要注意CPU_SET里填的是逻辑核编号超线程开启时相邻两个逻辑核共享同一物理核的执行资源扫可扩展性的时候容易在 2 的幂次上出现假平台期。稳妥做法是先用lscpu -e看清 CPU 到 core 的映射再决定绑哪些编号。4. 并发度、耦合性与 Cache 效率的采集方法4.1 三个指标怎么定义HTC-MicroBench 从三个角度刻画被测处理器作业并发性看的是多核能不能真的同时推进任务量化方式是相同作业数下线程数从 1 翻到 N 的吞吐增长率作业间的耦合性看的是任务之间有没有意外的资源争抢量化方式是观察同一份作业在单线程和多线程下的单作业延迟差Cache 使用效率看的是访存行为量化方式是 LLC 缺失率和每千条指令的缺失数。指标采集事件判读方向并发度吞吐 / 线程数曲线曲线斜率越接近线性越好耦合性单作业延迟差、context-switches差值越小说明干扰越少Cache 效率cache-misses、LLC-load-misses缺失率越低越好实时性尾部延迟 p99关注抖动而非均值四个指标一起看才有效。只看吞吐遇到瓶颈是核间争抢还是访存判断不出来只看缺失率又不知道缺失对吞吐的实际影响有多大。4.2 用 perf 采事件# 每 1 秒输出一次计数覆盖流水线、Cache 和调度三类事件 perf stat -e cycles,instructions,cache-references,cache-misses,\ LLC-loads,LLC-load-misses,context-switches \ -I 1000 -- ./htc_run 4096 8-I 1000让计数按秒切段输出能看出运行期的波动而不是一个平均值如果只看总平均值前几秒的冷启动会被后面的稳态稀释掉。LLC-loads这一对事件比cache-misses粒度更细专门反映最后一级缓存的访存压力高通量负载的瓶颈通常就落在这里。注意context-switches要结合 nthreads 一起看线程数没超核数却频繁切换说明绑核没生效或者有别的进程在抢。每指令缺失数这么算# 把 perf 输出里的两个计数相除得到 MPKI awk /LLC-load-misses/ {m$1} /instructions/ {i$1} END {printf MPKI%.4f\n, m/(i/1000)}4.3 扫线程数的批量脚本import subprocess, re results [] for t in [1, 2, 4, 8, 16, 32]: p subprocess.run([perf, stat, -e, cycles,LLC-load-misses, --, ./htc_run, 4096, str(t)], capture_outputTrue, textTrue) m re.search(rchecksum(\d), p.stdout) results.append((t, m.group(1) if m else N/A)) base None for t, _ in results: print(fthreads{t}) # 吞吐以 njobs / wall_time 换算再除单线程值得加速比脚本本身只负责把六档线程数跑一遍真正的分析在跑完之后把每个线程数下的墙钟时间换算成吞吐再除以单线程吞吐就是加速比。注意测量前先跑一轮预热把页表、分支预测器和缓存都填一遍否则第一档数据会明显偏低后面画出来的曲线起点是错的。提示采集时关掉其它重负载进程perf本身的采样开销在高并发下不可忽略可以先用perf stat的默认采样频率跑一遍确认数据抖动在接受范围内再加大采集密度。5. 从加速比曲线拐点反推微体系结构瓶颈拿到加速比曲线之后能读出的东西比一个数字多得多。拐点出现得早说明瓶颈在共享资源上通常是 LLC 带宽或者一致性协议流量拐点之后曲线往下掉说明线程数超过了硬件并行能力调度和上下文切换开始吃收益。把 13 个 Workload 的曲线叠在一张图上数据处理类的拐点普遍靠后实时交互类的拐点靠前——这和它们工作集大小的分布是一致的。TILE-Gx 和 Xeon 的对比最能说明问题。Xeon 靠深乱序执行和较大的 LLC在低线程数下每核性能占优曲线起点高TILE-Gx 是众核结构单核弱但核数多线程数拉高之后才把差距追回来甚至反超。如果只测 8 线程结论会是 Xeon 明显更快但这个结论对高通量场景是误导性的——真实部署的并发度远不止 8。定位瓶颈有个具体的操作改工作集大小扫 Cache 边界。# 同一份负载工作集分别设为 LLC 容量的 1/4、1/2、1 和 2 倍 for ws in 8 16 32 64; do ./htc_run 4096 16 --working-set-mb $ws | tail -1 done工作集从小于 LLC 涨到大于 LLC 的那一刻缺失率会陡升吞吐曲线同步出现一个台阶。如果台阶出现的位置和你根据 LLC 容量估算的位置吻合说明瓶颈确实在末级缓存如果台阶提前出现那多半是预取器或者 TLB 出了问题得去看dTLB-load-misses。另一条排查路径是固定线程数、只改作业粒度粒度细到几 KB 的时候如果 context-switches 明显上涨而吞吐不见长说明线程管理和任务分发的开销已经超过了作业本身这时候该调的是每线程批量取件的数量而不是继续加线程。还有一个容易被忽略的细节报告加速比的时候要标明基线。单线程基线用的是同一个二进制、同一份输入、同样的绑核策略三个条件差一个加速比就会虚高。真实项目里我一般把基线数据和各档数据写进同一份 CSV 一起归档方便后面复算。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门