明经CAD社区

 找回密码
 注册

QQ登录

只需一步,快速开始

搜索
查看: 642|回复: 6

[图形系统] 基础篇-说说硬件和总结优化

[复制链接]
发表于 2024-8-27 19:56:09 | 显示全部楼层 |阅读模式
本帖最后由 你有种再说一遍 于 2024-9-13 10:00 编辑

不懂硬件是很难写出比懂硬件更好的代码的.
遇到性能不好的时候,就可以来看看.

数据预取:无论是硬盘还是内存,都存在预取,因为CPU实在太快了,其他硬件运算频率不如CPU速度.
内存硬件:内部有多路复用器,所以不是8根导线进入取值就取了,它进入之后还得扫描和等待刷新电容工作完成之后才取值,因此顺序访问才是最快.
电源频率:引起的大小核调度错误,要防止一核有难多核围观,得设置线程亲和性,而且可以命中核心内的L2缓存.
SIMD:CPU有没有SIMD,SIMD版本支持程度,AVX512会不会降频.可以加个配置跳过某些CPU用AVX512,改用SSE,SSE2,SSE3,AVX,AVX2...
硬盘:设置对齐方式,所以结构体对齐为页(通常4k).PG数据库为什么选择了尾追加策略的LSM树,就是因为它在硬件层面就比mysql的B+树好太多了,号称穷人核弹.
CPU核心的连接方式,星型,环形,矩阵,他们虽然在代码层面没啥意义,但是涉及通讯就有意义了.

多线程分为:
网络上面是IO密集:线程复用,降低句柄耗尽,
linux上面的BIO,NIO,AIO以及poll,select,epoll的调用概念.nettiy组件.
图形学上面需要的是CPU密集计算.

算法层面的优化,减少时间复杂度就经常说了.
容器降低申请内存次数,拷贝次数,复用.
无脑用HashMap的KV结构.
位图bitmap:记录内存是否使用.进而延伸出多个哈希的布隆过滤器记录数据是否使用.

通用的优化策略:
1,缩短数据类型:
能byte(1字节)就不要short(2字节),
能short就不要int(4字节),
能int就不要long(8字节).
以及还有整数表示浮点数的定点数.

2,减少函数:内联函数,递归改循环,开辟栈帧会引入新指令和新数据,可能把前面的L2缓存顶到L3去了,L3顶出去就cache miss了.

3,数据对齐:从页表开始,都是整整齐齐的4k.这甚至到硬盘的预取,所谓的512(字节)对齐,4k(4096字节)对齐,就决定了预取颗粒大小,这通常发生在mmap映射磁盘到内存,然后操作系统取内存的时候.同时readFile其实也会.

4,数组预取:数据从硬盘拷贝到主存,主存拷贝到缓存,都是连续的数据结构更好,所以没有比同类型数值数组更好的结构了.

5,SIMD:因为取值/运算这两步并行,它的寄存器和其他寄存器也是分开的,通过组合:多线程并行+数值数组预取+循环展开分支流水线+SIMD指令,得到一个最快的"烤内存方案",直接拉满总线带宽.

6,减少分支预测失败:JIT特化或者SIMD.

7,编译器优化:
太多基础的,死代码消除,无用代码消除,
变量提升:提取到头部声明,防止入栈期间内存不足.
常量传播:求值之后替换变量.
常量折叠:直接编译时候就求循环内的常数,就连-o1也会进行,所以不应该在代码上面有达夫设备(Duff’s Device)的代码.
循环展开:c#需要手写展开,利用CPU的分支流水线技术.每个变量都要独立(c++能数组,c#只能变量并且要用指针方案),因为变量不关联就是流水线的分支,最后才聚合累加.
例如数组全部求和,可以设置4个变量,每次递进4,加到对应的变量上,最后4个变量累加.
循环融合:如果有两个循环分别计算数组a和b的平方和,并且这两个循环是连续的,编译器可以将它们合并为一个循环,同时计算两个平方和.
循环拆分:如果一个循环既更新了一个数组,又计算了另一个数组的值,而这些操作之间存在数据依赖,循环拆分可以将更新操作和计算操作分开,以减少依赖并提高并行度.
循环向量化:转为SIMD指令

8,多线程并行:每个核心都有独立SIMD寄存器和其他寄存器.

9,缓存一致性协议MESI:
volatile:
这个和编译器优化是有关系的,编译器会觉得你某个变量赋值后不修改,然后去帮你删掉不可达的代码了,结果是另一个线程修改这个变量后,其他代码是可达的,因此需要加这个关键字.
你的变量需要volatile才能给不同线程看见,不然它们被拷贝到内核缓存了,而你再对某个线程的变量修改是无效的.
它的硬件原理并不是每次都去主存取值,而是拷贝主存到核心缓存之后,当修改的时候,会通过总线发送给其他核心一个修改信号,其他核心通过检测信号再去主存拷贝到自己的缓存,核心内如果没有用到,就不去拷贝,这样可以减少访问主存.
也就是可见,但是修改时候是不及时的,修改要通过内存屏障和锁才行.
内存屏障:
内存屏障是一种指令,用于控制CPU指令的执行顺序,防止编译器和处理器重排序.
内存屏障通常用于与volatile变量一起使用,以确保在访问volatile变量前后的内存操作顺序.
锁:
锁用于同步多个线程对共享资源的访问,保证在同一时刻只有一个线程可以访问特定的代码段或资源.
锁提供了原子性和可见性保证.
使用锁可以防止多个线程同时进入临界区,从而避免竞态条件和数据不一致的问题.

有什么是volatile+内存屏障,但是不用锁呢?
a:无锁编程CAS上面就有用到了.
b:多线程并行的终止标记.

10,缓存伪共享:它被誉为了无声的性能杀手,缓存是一行的,一行可以容纳好多个变量,如果你频繁修改一个对象,那么另一个对象也在一行,会发生等另一个对象释放缓存之后,你才能抢占到锁.
解决方案:填充/数据对齐/无锁结构

11,线程亲和性:CPU的超线程,也就是大家看到的八核十二线程的宣传语.
同核心是共享L2缓存的,而不同核心之间还可以通过L3缓存去实现.要做到这一点需要绑定两个线程到一个核心上面.

线程亲和性绑定到核心
```c#
using System;
using System.Runtime.InteropServices;
using System.Threading;

class Program
{
    // 获取当前线程的CPU核心编号
    [DllImport("kernel32.dll")]
    private static extern uint GetCurrentProcessorNumber();

    // 设置线程绑定到核心
    [DllImport("kernel32.dll")]
    private static extern uint SetThreadAffinityMask(IntPtr threadHandle, uint processorMask);

    static void Main() {
        // 创建一个线程
        Thread thread = new Thread(DoWork);
        // 启动线程
        thread.Start();
        // 等待线程启动
        thread.Join();

        // 设置线程亲和性,将线程绑定到CPU核心0
        SetThreadAffinityMask(thread.Handle, 1);

        // 继续线程执行
        thread.Start();
    }

    static void DoWork() {
        for (int i = 0; i < 5; i++) {
            Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId} is running on CPU {GetCurrentProcessorNumber()}");
            Thread.Sleep(1000);
        }
    }
}
```

12,磁盘文件处理:读取文件用mmap.
发送文件用0拷贝,DMA直接访问内存.

13,并行的颗粒度
开n个线程处理n个文件,开n个线程处理一个文件的颗粒度是不一样的.
MKL数学库矩阵:矩阵分块拼凑大矩阵,分块颗粒度给不同的线程.
利用核心绑定,Z字访问以命中缓存行,由此解决了需要转置矩阵才能命中缓存行.Z字命中缓存行是因为邻近两个块的元素已经被读取进入了CPU三个缓存区.
你会发现就是MapReduce思想:先分区,并行分区的任务,最后合并.
http://bbs.mjtd.com/thread-190857-1-1.html
http://bbs.mjtd.com/thread-190913-1-1.html
(完)

评分

参与人数 1明经币 +1 收起 理由
baitang36 + 1 很给力!

查看全部评分

发表于 2024-8-27 22:40:16 | 显示全部楼层
感觉,你进错了论坛
回复 支持 1 反对 0

使用道具 举报

 楼主| 发表于 2024-8-27 22:55:39 | 显示全部楼层
satan421 发表于 2024-8-27 22:40
感觉,你进错了论坛

不会,因为敲lisp的没有看过别的语言,自然想不出来别的语言速度为什么那么快.
同时还有IFox的人需要看.
发表于 2024-8-28 10:31:26 | 显示全部楼层
学计算机原理都没给细讲缓存和分支预测,只讲了内存的分页管理,内存中没有触发异常,异常再去处理虚拟内存。不过编译器编译的汇编代码确实有好多冗余代码,最多的就是每个函数执行前后都要进行栈的平衡检查。
 楼主| 发表于 2024-8-28 16:42:32 | 显示全部楼层
springwillow 发表于 2024-8-28 10:31
学计算机原理都没给细讲缓存和分支预测,只讲了内存的分页管理,内存中没有触发异常,异常再去处理虚拟内存 ...

那些偏向反汇编技术,不是很好学,我没学懂,嘻嘻
发表于 2024-8-28 21:22:47 | 显示全部楼层
你有种再说一遍 发表于 2024-8-27 22:55
不会,因为敲lisp的没有看过别的语言,自然想不出来别的语言速度为什么那么快.
同时还有IFox的人需要看.

选择lisp的估计大概率对效率没什么要求,或者短时间之内lisp还没成为瓶颈
 楼主| 发表于 2024-8-28 21:43:02 | 显示全部楼层
本帖最后由 你有种再说一遍 于 2024-8-28 21:50 编辑
satan421 发表于 2024-8-28 21:22
选择lisp的估计大概率对效率没什么要求,或者短时间之内lisp还没成为瓶颈

不做就没有瓶颈憋,这些问题lisp直接跳过了.
对比填充找到一致,替换全图.
对比两份图纸差异,
找10万图元的全部链条.
这些lisp做起来都是一分钟起步,而c#可以2秒.

这种和win32交互的鼠标钩子.
拉伸填充.
嵌入式文档栏.
字体渲染.
也没看见lisp有人做好的...


服务系统,
监控某个文件夹,实现自动打印全部PDF.
这样lisp也做不到呀...
您需要登录后才可以回帖 登录 | 注册

本版积分规则

小黑屋|手机版|CAD论坛|CAD教程|CAD下载|联系我们|关于明经|明经通道 ( 粤ICP备05003914号 )  
©2000-2023 明经通道 版权所有 本站代码,在未取得本站及作者授权的情况下,不得用于商业用途

GMT+8, 2024-11-22 18:57 , Processed in 0.176848 second(s), 24 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表