You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 12:24:59