Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >我只用了 3 行 C#:CPU 缓存行就将我的 API 速度提高了一倍

我只用了 3 行 C#:CPU 缓存行就将我的 API 速度提高了一倍

作者头像
郑子铭
发布于 2025-03-24 07:08:48
发布于 2025-03-24 07:08:48
7400
代码可运行
举报
运行总次数:0
代码可运行

回想一下你上次优化 .NET 应用程序的情景。你可能关注了算法、数据库查询,或者异步模式。但如果我告诉你,仅仅改变数据在内存中的布局,就能让你的应用程序性能翻倍,你会怎么想?这并不是理论上的假设——我们最近在调查高流量 API 的性能问题时,就深刻体会到了这一点。

现代 CPU 的速度非常快!!!但它们大部分时间都在等待。等待什么呢?内存。虽然 CPU 可以在纳秒级的时间内执行指令,但从主内存中获取数据却需要数百个 CPU 周期(这是我们可以控制的部分)。为了弥补这一差距,CPU 使用了缓存层次结构——小而快的内存区域,用于将频繁访问的数据保存在处理核心附近。

问题:一个看似无辜的结构体布局

我们有一个看似无害的结构体,用于处理用户会话数据:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public structSessionData
{
    publicbool IsAuthenticated;    // 1 字节
    publicstring Username;         // 8 字节(引用)
    publicbyte SecurityLevel;      // 1 字节
    publicDateTime LastAccess;     // 8 字节
    publicGuid SessionId;          // 16 字节
}

这个结构体看起来干净且逻辑清晰,对吧?每个字段都按其用途分组。但当我们在负载下分析应用程序时,发现了一个令人惊讶的现象。尽管我们的数据应该完全适合缓存,但 CPU 却花费了大量时间等待内存。

基准测试不会说谎

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[MemoryDiagnoser]
publicclassCacheAlignmentBenchmark
{
    privateSessionData[] originalData;
    privateOptimizedSessionData[] alignedData;
    privateconstint ArraySize =10_000;

    [GlobalSetup]
    publicvoidSetup()
    {
        originalData =newSessionData[ArraySize];
        alignedData =newOptimizedSessionData[ArraySize];
       
        for(int i =; i < ArraySize; i++)
        {
            originalData[i]=newSessionData
            {
                IsAuthenticated =(i %==),
                Username =$"user{i}",
                SecurityLevel =(byte)(i %),
                LastAccess = DateTime.UtcNow,
                SessionId = Guid.NewGuid()
            };
        }
    }

    [Benchmark(Baseline = true)]
    publicvoidProcessOriginalLayout()
    {
        for(int i =; i < ArraySize; i++)
        {
            if(originalData[i].IsAuthenticated)
            {
                Process(originalData[i]);
            }
        }
    }

    [Benchmark]
    publicvoidProcessAlignedLayout()
    {
        for(int i =; i < ArraySize; i++)
        {
            if(alignedData[i].IsAuthenticated)
            {
                Process(alignedData[i]);
            }
        }
    }
}

优化后的版本运行速度快了 2.3 倍!不是 2.3%,而是 2.3 倍……😯

这种性能提升让你不得不重新检查基准测试,因为它看起来好得令人难以置信。

首先了解 CPU 缓存行

要理解为什么会发生这种情况,我们需要可视化 CPU 如何与内存交互。想象一下,你在一个图书馆工作,书籍(你的数据)存储在一个遥远的仓库(主内存)中。你不能只取单页,而必须一次请求整个书架的书(缓存行)。如果你需要的页面分散在不同的书架上,你将花费更多时间在仓库之间来回奔波,而不是真正阅读。

这正是我们 CPU 中发生的情况。当我们访问 SessionData 结构体时,由于数据组织不当,CPU 需要获取多个缓存行。这就像把一本书的页面分散在仓库的不同书架上。

现在让我们解决这个问题

以下是我们如何修复 SessionData 结构体的方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[StructLayout(LayoutKind.Sequential, Pack = )]
publicstructOptimizedSessionData
{
    publicGuid SessionId;          // 16 字节
    publicDateTime LastAccess;     // 8 字节
    publicstring Username;         // 8 字节(引用)
    publicbyte SecurityLevel;      // 1 字节
    publicbool IsAuthenticated;    // 1 字节
    privatebyte _padding1;         // 1 字节
    privatebyte _padding2;         // 1 字节
}

注意以下变化:

  • 我们按字段大小(从大到小)重新排序
  • 添加了显式填充以确保正确对齐
  • 使用 StructLayout 属性来控制内存布局

我知道你可能仍然对实际实现和原因感到困惑……

让我们看一些实际场景

在微软,.NET 团队对核心运行时应用了类似的优化。在 ThreadPool 实现中,对结构体布局和缓存对齐的仔细关注带来了显著的吞吐量提升。同样的原则也适用于像 Unity 这样的高性能游戏引擎,结构体布局可能意味着流畅游戏体验和明显卡顿之间的区别。

考虑我们生产环境中的 API:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public classUserSessionCache
{
    privatereadonlySessionData[] _sessions;
    privatereadonlyint _capacity;

    publicUserSessionCache(int capacity)
    {
        _capacity = capacity;
        _sessions =newSessionData[capacity];
    }

    publicboolTryGetSession(Guid sessionId,outSessionData session)
    {
        for(int i =; i < _capacity; i++)
        {
            if(_sessions[i].SessionId == sessionId)
            {
                session = _sessions[i];
                returntrue;
            }
        }
        session =default;
        returnfalse;
    }
}

在我们的高流量应用程序中,这段代码每秒被调用数千次。通过优化结构体布局,我们在不改变任何业务逻辑的情况下,将 API 响应时间减少了 47%。

🚦 本文的讨论部分已开放

什么时候你应该关心缓存行?

并非每个应用程序都需要这种级别的优化。如果你正在构建一个典型的中等流量的 CRUD 应用程序,你的性能问题更可能出现在数据库访问或网络延迟上。然而,在以下情况下,你应该考虑缓存行优化:

  • 你的应用程序处理大型结构体数组
  • 你有高吞吐量场景,每秒处理数百万次操作
  • 你正在构建性能关键的基础设施组件
  • 你的性能分析器显示高 CPU 缓存未命中率

例如,交易系统通常每秒处理数百万次市场数据更新。在这些场景中,正确的结构体布局可能是抓住市场机会和错失机会之间的区别。

这篇文章不仅仅是几个小时的写作,而是多年编程和技术学习的结晶……

如果你想表示感谢 | 请我喝杯咖啡🖤

测量缓存性能的工具和技术

在优化之前,你需要进行测量。以下是一些实用工具:

  • Windows 性能计数器(perfmon.exe)——查看缓存相关计数器
  • Intel VTune Profiler——提供详细的缓存分析
  • 带有硬件计数器的 BenchmarkDotNet:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
[HardwareCounters(
    HardwareCounter.CacheMisses,
    HardwareCounter.BranchMispredictions)]
public class CacheAlignmentBenchmark
{
    // ... 基准测试代码 ...
}

🚥 最佳实践

在优化缓存行时,请遵循以下准则:

  • 从测量开始——不要盲目优化
  • 对性能关键的数据结构使用结构体
  • 按字段大小从大到小排序
  • 考虑使用 StructLayout 属性
  • 注意多线程场景中的伪共享
  • 必要时添加填充以对齐缓存行

例如:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public classCacheFriendlyBuffer<T>whereT:struct
{
    privatereadonlyT[] _items;
    privatereadonlyint _cacheLineSize;
    privatereadonlyint _itemsPerCacheLine;

    publicCacheFriendlyBuffer(int capacity,int cacheLineSize =)
    {
        _cacheLineSize = cacheLineSize;
        _itemsPerCacheLine = _cacheLineSize / Unsafe.SizeOf<T>();
        _items =newT[capacity];
    }

    publicrefTGet(int index)
    {
        var alignedIndex =(index / _itemsPerCacheLine)* _itemsPerCacheLine;
        returnref _items[alignedIndex +(index % _itemsPerCacheLine)];
    }
}

展望未来:未来的考虑

随着 CPU 架构的发展,缓存优化变得更加关键。现代处理器正在增加更多的核心和更大的缓存行,这使得正确的内存布局变得越来越重要。异构计算和非统一内存访问(NUMA)架构的出现为缓存优化增加了另一层复杂性。

考虑以下趋势:

  • ARM 处理器在服务器领域可能有不同的缓存行大小
  • 像 DDR5 这样的新内存技术会影响缓存层次结构
  • 虚拟机可能具有不同的缓存特性

缓存行优化并不是银弹,但它是你性能优化工具箱中的一个强大工具。从测量应用程序的缓存性能开始,识别关键数据结构,并优化它们的布局。性能提升可能是巨大的,而你学到的原则将使你成为一名更好的开发者。

📝 在高性能计算中,理解代码如何与硬件交互与理解算法和数据结构同样重要。缓存行优化是理论与实践的结合,有时,简单的结构体重排序可以胜过数周的算法优化。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-03-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 DotNet NB 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
C# StructLayout
在C#中,内存布局对于性能优化和与非托管代码的互操作性至关重要。StructLayout特性允许开发者控制结构在内存中的布局方式。本文将深入探讨StructLayout的用途、选项及其在实际应用中的意义。
JusterZhu
2025/01/23
1160
C# StructLayout
软硬件协同编程 - C#玩转CPU高速缓存(附示例)
好久没有写博客了,一直在不断地探索响应式DDD,又get到了很多新知识,解惑了很多老问题,最近读了Martin Fowler大师一篇非常精彩的博客The LMAX Architecture,里面有一个术语Mechanical Sympathy,姑且翻译成软硬件协同编程(Hardware and software working together in harmony),很有感悟,说的是要把编程与底层硬件协同起来,这样对于开发低延迟、高并发的系统特别地重要,为什么呢,今天我们就来讲讲CPU的高速缓存。
justmine
2019/02/15
7360
CPU Cache简介
真空中光速为299,792,458米/秒,目前,Intel的i7频率可以达到4GHz,简单换算一下,可以得出结论:光(电流)在一个Cycle内移动的距离约为0.075米。显然,目前的内存条的芯片(反正两面。约为3.75cm)大大超过了这个长度,换句话说,理论上,在一个Cycle内内存条上总有一个位置是我们无法触摸。
Peter Lu
2018/09/30
1K0
CPU Cache简介
CPU Cache与False Sharing
现代多核CPU会在每个核心上加上一个较小的SRAM高速缓存存储器称为:L1高速缓存,其中L1缓存由分为dcache数据缓存,icache指令缓存。在L1缓存的下级加一个较大的L2高速缓存, 然后会再L2之下加一个多核共享的L3高速缓存。它们之间的逻辑结构大概是这样的:
Orlion
2024/09/02
1620
CPU Cache与False Sharing
Disruptor-NET和内存栅栏
Disruptor-NET算法(是一种无锁算法)需要我们自己实现某一种特定的内存操作的语义以保证算法的正确性。这时我们就需要显式的使用一些指令来控制内存操作指令的顺序以及其可见性定义。这种指令称为内存栅栏。 内存一致性模型需要在各种的程序与系统的各个层次上定义内存访问的行为。在机器码与的层次上,其定义将影响硬件的设计者以及机器码开发人员;而在高级语言层次上,其定义将影响高级语言开发人员以及编译器开发人员和硬件设计人员。即,内存操作的乱序在各个层次都是存在的。这里,所谓的程序的执行顺序有三种: (1)程序顺序
张善友
2018/01/29
8480
C++一分钟之-缓存行与伪共享问题
在计算机科学中,缓存是一个至关重要的概念,它能够显著提高数据访问速度。然而,缓存的使用并非没有问题,其中最著名的问题之一就是伪共享。本文将深入浅出地介绍缓存行与伪共享问题,包括常见问题、易错点以及如何避免这些问题。
Jimaks
2024/07/12
1700
不是我吹,20M的压缩文件我只用了1秒!
有一个需求需要将前端传过来的10张照片,然后后端进行处理以后压缩成一个压缩包通过网络流传输出去。之前没有接触过用Java压缩文件的,所以就直接上网找了一个例子改了一下用了,改完以后也能使用,但是随着前端所传图片的大小越来越大的时候,耗费的时间也在急剧增加,最后测了一下压缩20M的文件竟然需要30秒的时间。
IT大咖说
2020/04/21
5420
不是我吹,20M的压缩文件我只用了1秒!
Go语言中常见100问题-#92 Writing concurrent code that leads to false ...
在Go语言中常见100问题-#91 Not understanding CPU caches中讨论了缓存基本概念,可以看到一些特定的缓存(通常是 L1 和 L2)并不在所有逻辑内核之间共享,而是属于一个特定物理内核。这种特殊性会产生一些影响,比如并发时伪共享,这会导致性能显著下降。下面通过一个具体例子来说明什么是伪共享,以及如何预防这种情况发生。
数据小冰
2023/12/13
1920
Go语言中常见100问题-#92 Writing concurrent code that leads to false ...
一招MAX降低10倍,现在它是我的了| 京东零售技术团队
到家门店系统,作为到家核心基础服务之一,门店C端接口有着调用量高,性能要求高的特点。
用户6256742
2024/06/30
1850
一招MAX降低10倍,现在它是我的了| 京东零售技术团队
C# 中如何计算一个实例占用多少内存?
我们都知道CPU和内存是程序最为重要的两类指标,那么有多少人真正想过这个问题:一个类型(值类型或者引用类型)的实例在内存中究竟占多少字节?我们很多人都回答不上来。
郑子铭
2023/08/30
7210
C# 中如何计算一个实例占用多少内存?
使用Go实现健壮的内存型缓存
本文介绍了缓存的常见使用场景、选型以及注意点,比较有价值。 译自:Implementing robust in-memory cache with Go
charlieroro
2022/05/20
8160
使用Go实现健壮的内存型缓存
Go语言中常见100问题-#91 Not understanding CPU caches
机械同理心(mechanical sympathy)是三届F1世界冠军杰基·斯图尔特 (Jackie Stewart) 创造的一个术语。
数据小冰
2023/11/29
2460
Go语言中常见100问题-#91 Not understanding CPU caches
深入理解 C++17 的缓存行接口
在当今的多核处理器架构体系中,缓存行(Cache Line)作为 CPU 缓存操作的基础单元,扮演着至关重要的角色。一般而言,缓存行的大小普遍设定为 64 字节。这也就意味着,当 CPU 对内存进行访问操作时,它会以 64 字节为一个数据块单位,将内存中的数据加载到缓存当中。这种数据加载机制在很大程度上提高了内存访问的效率,使得 CPU 能够更快速地获取所需数据。然而,如同硬币的两面,这种机制也可能会引发一些性能方面的问题,其中比较典型的就是伪共享(False Sharing)和缓存行破坏(Cache Line Thrashing)现象。为了有效解决这些问题,C++17 引入了缓存行接口,这一特性为开发者提供了更有力的工具,帮助他们对代码性能进行更精细的优化。
码事漫谈
2025/02/18
1440
深入理解 C++17 的缓存行接口
一行“无用”的枚举反使Rust执行效率提升10%,编程到最后都是极致的艺术
最近不少读者都留言说博客中的代码越来越反哺归真,但讨论的问题反倒越来越高大上了,从并发到乱序执行再到内存布局各种放飞自我。
beyondma
2021/07/11
8500
Go高性能编程EP3: 内存对齐
本文是Go语言高性能编程第三篇,分析了为什么需要内存对齐,Go语言内存对齐的规则,以及实际例子中内存对齐的使用,最后分享了两个工具,帮助我们在开发过程中发现内存对齐问题。
萝卜要努力
2025/03/07
850
Go高性能编程EP3: 内存对齐
手摸手Go 你的内存对齐了吗?
谈到内存对齐,早年间玩Java的时候就能偶尔打打交道,为此Java8还提供了个语法糖@Contended来帮助我们解决高速缓存cacheline内存未对齐的伪共享问题。不过Go目前涉及到类似问题,比如内存对齐带来的原子操作的问题还是需要手动处理下,毕竟Russ Cox大佬也发话了
用户3904122
2022/06/29
5800
手摸手Go 你的内存对齐了吗?
热度碾压 Java、C#、C++的 Python,为什么速度那么慢?
眼下 Python 异常火爆,不论是 DevOps、数据科学、Web 开发还是安全领域,都在用 Python——但是它在速度上却没有任何优势。
CDA数据分析师
2018/08/14
2.3K0
热度碾压 Java、C#、C++的 Python,为什么速度那么慢?
部署太慢,我们用 Warm Docker 容器将速度提高了 5 倍
作者 | Shalabh Chaturvedi 译者 | Sambodhi 策划 | 褚杏娟 背   景 我们使用 Serverless Dagster Cloud 来开发和部署 Dagster 代码,无需设置本地开发环境或任何云基础架构。当提交更改到 GitHub 时,GitHub Action 会直接构建和部署代码到 Dagster Cloud,然后可以在界面上查看并与 Dagster 对象进行交互。Dagster Cloud 可以利用一个远程环境来共享部署,并且可以利用自动创建的临时环境
深度学习与Python
2023/04/01
7170
部署太慢,我们用 Warm Docker 容器将速度提高了 5 倍
.NET性能优化-使用结构体替代类
我们知道在C#和Java明显的一个区别就是C#可以自定义值类型,也就是今天的主角struct,我们有了更加方便的class为什么微软还加入了struct呢?这其实就是今天要谈到的一个优化性能的Tips使用结构体替代类。 那么使用结构体替代类有什么好处呢?在什么样的场景需要使用结构体来替代类呢?今天的文章为大家一一解答。注意:本文全部都以x64位平台为例
InCerry
2022/11/14
5300
写Java也得了解CPU–CPU缓存
CPU,一般认为写C/C++的才需要了解,写高级语言的(Java/C#/pathon…)并不需要了解那么底层的东西。我一开始也是这么想的,但直到碰到LMAX的Disruptor,以及马丁的博文,才发现写Java的,更加不能忽视CPU。经过一段时间的阅读,希望总结一下自己的阅读后的感悟。本文主要谈谈CPU缓存对Java编程的影响,不涉及具体CPU缓存的机制和实现。
哲洛不闹
2018/09/19
1.1K0
写Java也得了解CPU–CPU缓存
相关推荐
C# StructLayout
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验