SEH会增加内存读写开销吗?两种缓冲区检查方法性能差异排查
缓冲区检查方法性能差异问题分析
问题背景
我正在对比两种缓冲区边界检查方法:
- 分支检查法:每次迭代时显式判断是否到达缓冲区末尾
- Guard Page+SEH法:利用内存保护页触发异常,通过结构化异常处理捕获并终止循环
理论上Guard Page方法无需在循环中插入分支指令,应该更快,但实际测试结果完全相反——尤其是存储操作场景下,SEH方法的耗时是分支检查法的5倍。
基准测试结果(10次试验平均值)
branch + load: 58947659.3 branch + store: 15234306.6 seh + load: 84706608.6 seh + store: 84822314.3
测试代码
#include <Windows.h> #include <stdio.h> #define BUFFER_SIZE 16ull * 1024ull * 1024ull * 1024ull // 注释此行切换为存储操作 #define LOAD // 注释此行切换为SEH方法 #define USE_BRANCH int main() { HANDLE consoleHandle = GetStdHandle(STD_OUTPUT_HANDLE); char* memory = VirtualAlloc(NULL, BUFFER_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (memory == NULL) return 0; unsigned long long total = 0; char* memoryStart = memory; #ifdef USE_BRANCH LARGE_INTEGER perfcountBefore; QueryPerformanceCounter(&perfcountBefore); while (memory < memoryStart + BUFFER_SIZE) { #ifdef LOAD total += *memory; #else (*memory)++; #endif memory++; } LARGE_INTEGER perfcountAfter; QueryPerformanceCounter(&perfcountAfter); char buffer[30]; int stringlength = _snprintf_s(buffer, 30, _TRUNCATE, "operation took %i\n", perfcountAfter.QuadPart - perfcountBefore.QuadPart); WriteConsoleA(consoleHandle, buffer, stringlength, NULL, NULL); #else SYSTEM_INFO si; GetSystemInfo(&si); DWORD garbage; VirtualProtect(memory + BUFFER_SIZE - si.dwPageSize, si.dwPageSize, PAGE_READWRITE | PAGE_GUARD, &garbage); LARGE_INTEGER perfcountBefore; QueryPerformanceCounter(&perfcountBefore); __try { while (1) { #ifdef LOAD total += *memory; #else (*memory)++; #endif memory++; } } __except (EXCEPTION_EXECUTE_HANDLER) { while (memory < memoryStart + BUFFER_SIZE) { #ifdef LOAD total += *memory; #else (*memory)++; #endif memory++; } LARGE_INTEGER perfcountAfter; QueryPerformanceCounter(&perfcountAfter); char buffer[30]; int stringlength = _snprintf_s(buffer, 30, _TRUNCATE, "operation took %i\n", perfcountAfter.QuadPart - perfcountBefore.QuadPart); WriteConsoleA(consoleHandle, buffer, stringlength, NULL, NULL); } #endif return total; }
性能差异的核心原因
1. 分支预测的极致优化
现代CPU的分支预测器对while (memory < memoryStart + BUFFER_SIZE)这种固定边界的线性循环分支预测准确率接近100%。CPU会提前预取指令、维持流水线满负荷运行,实际分支几乎没有额外开销——这种场景下分支检查的成本可以忽略。
2. SEH异常处理的巨大开销
当触发Guard Page异常时,系统需要执行一系列高成本操作:
- 暂停当前线程的执行流程
- 从用户态切换到内核态处理异常
- 遍历线程的异常处理链,定位到你的
__except块 - 恢复线程上下文,切回用户态执行异常处理代码
这一套流程的开销远大于一个被完美预测的分支。你的缓冲区是16GB,最终必然触发一次异常,这个单次异常的高额开销会平摊到整个循环中——而循环本身只是简单的单字节操作,异常开销的占比被极度放大。
3. 存储操作的额外放大效应
存储操作(*memory)++本身会触发更多缓存行为(如缓存行修改、回写),而Guard Page异常触发时,内核需要修改内存页的保护属性,这与存储操作的缓存交互会进一步增加延迟,导致存储场景下的SEH开销比读场景更夸张。
4. Guard Page的设计场景偏差
Guard Page的初衷是检测意外的越界访问(如bug导致的缓冲区溢出),而非作为常规循环的边界检查手段。它的优势是无需在循环中插入任何检查指令,但代价是异常触发时的极高开销——只有当循环次数极多、且分支检查的预测失败率很高时,Guard Page才可能体现优势,但你的场景完全不满足这个条件。
补充验证建议
- 尝试将缓冲区缩小到几KB,观察分支预测失效时两种方法的性能变化(此时分支检查会出现更多预测失败,开销会明显上升)
- 移除异常处理块中额外的循环(你的代码在触发异常后还会遍历剩余页面,这部分也会增加SEH方法的总耗时)
内容的提问来源于stack exchange,提问作者Badasahog
相关产品推荐
相关产品推荐

