- 博客(163)
- 收藏
- 关注
原创 《计算机体系结构:量化研究方法》第一章——缓存优化
本文深入解析了存储器层次结构中的高速缓存机制。首先介绍了缓存性能评估方法,通过存储器停顿周期公式量化缓存缺失对CPU执行时间的影响。重点探讨了缓存设计的四个核心问题:块放置策略(直接映射、全相联、组相联)、块查找机制(索引-标记-偏移)、替换算法(随机、LRU、FIFO)以及写入策略(直写/写回、写分配/非写分配)。最后以AMD Opteron处理器的64KB 2路组相联数据缓存为例,展示了实际应用中的缓存组织方式,采用LRU替换和写回策略。文章通过理论分析与实例说明,系统阐述了高速缓存的工作原理和性能优化
2026-01-08 09:42:30
914
原创 DWARF(一):调试格式概要
大多数现代编程语言都采用块结构:每个实体(例如类定义或函数)都包含在另一个实体中。这形成了词法作用域,名称仅在其定义的作用域内有效。要查找程序中某个符号的定义,需先在当前作用域中查找,然后依次在包含它的外层作用域中查找,直至找到该符号。同一名称可能在不同作用域中有多个定义。编译器自然会将程序内部表示为树状结构。例如,C程序文件可能包含多个数据定义、多个变量定义和多个函数;每个C函数内部可能包含多个数据定义,其后紧跟可执行语句;语句可能是复合语句,进而包含更多数据定义和可执行语句。
2025-12-02 17:28:49
684
原创 LuaJIT:MCode memory area
文章摘要:Trace在编译器中经历五个状态(START→RECORD→END→ASM→IDLE),完成从字节码到SSA IR再到机器码的转换。MCode内存区域采用RW^X权限管理,支持多区域4KB分页布局,通过MCLink链表连接。机器指令生成阶段需处理MCode区域上限问题,执行前需提交mcode并设置可执行权限。整个流程涉及IR优化、内存管理及权限控制,确保JIT编译的安全性和高效性。(149字)
2025-06-07 10:48:07
580
原创 LuaJIT:Garbage Collector Algorithms
本篇文章是对MakePall发表wili内容《》的翻译和扩展,因为原文是对LuaJIT2.xGC重要功能的简介和对LuaJIT3.0newGC的工作计划,所以它并不是系统性介绍GC的文章。希望以后能有精力系统性的对LuaJIT2.xGC做个总结。到目前(2025-1-17)为止,LuaJIT3.0尚未发布。看Make列的3.0plan,其中优化了很多功能,非常值得期待。但他也说了,3.0有可能永远不会发布,这将是非常遗憾的一件事情。
2025-01-17 15:56:31
1143
原创 基于Trace的类型特化动态语言JIT编译
动态语⾔JavaScript、Python 和 Ruby 等语⾔⾮常流⾏,因为它们表达能⼒强、⾮专业⼈⼠也能轻松使⽤,并且部署起来就像分发源⽂件⼀样简单。它们既可⽤于⼩型脚本,也可⽤于复杂的应⽤程序。例如,JavaScript 是客户端 Web 编程的事实标准并⽤于基于浏览器的⽣产⼒应⽤程序(例如 Google Mail、Google Docs 和 Zimbra Collaboration Suite)的应⽤程序逻辑。
2024-07-18 18:28:31
1556
原创 Golang编译优化——稀疏条件常量传播
常量传播(constant propagation)是一种转换,对于给定的关于某个变量xxx和一个常量ccc的赋值x←cx←c,这种转换用ccc来替代以后出现的xxx的引用,只要在这期间没有出现另外改变xxx值的赋值。如下图a的CFG所示,基本块B1中的指令b←3b←3将常量3赋给bbb,并且CFG中没有其他对bbb的赋值。图b是对图a做常量传播的结果,此时bbb。
2024-05-08 18:10:26
1689
4
原创 Golang编译优化——公共子表达式消除
公共子表达式消除(Common Subexpression Elimination,CSE)也有书上称为冗余表达式消除,旨在减少程序中重复计算相同表达式的次数,从而提高程序的执行效率。在程序中,如果同一个表达式在不同的地方多次出现并且具有相同的输入,则这个表达式就是一个公共子表达式。公共子表达式消除的目标是识别这些重复的表达式,并将它们计算一次,然后在需要时重用结果,而不是每次都重新计算。在这个例子中,表达式b + c在两个地方都出现了,它是一个公共子表达式。如果程序执行这两个语句,那么每次都重新计算。
2024-04-24 17:11:32
1836
3
原创 Golang编译优化——死代码删除
b *Blocki int死代码删除的在deadcode函数中实现,对于一个函数f,删除其死代码的过程如下:首先检查函数f是否经过寄存器分配(regalloc)。如果已经经过寄存器分配,那么无法进行死代码删除,因为寄存器分配可能生成多余的 SSA 代码,会导致一些必需的移动操作被消除。调用,找到函数f中可达的基本块,将其放入数组reachable。删除从不可达块到可达块的边,确保不会有从死代码到活跃代码的控制流传播。
2024-04-18 19:41:01
1474
原创 Golang编译优化——复制传播
以下是Go编译器对某个代码段编译生成的SSA IR摘选,对于Golang SSA IR的介绍我写了文章,但是在犹豫要不要发。编译器在中间代码生成和优化阶段,不可避免的会生成一些非必要的指令,如上面b3块中的。消除Copy指令的操作会遍历所有IR,迭代找到Copy指令的最终引用,将其替换到合适的位置。下列,引用参数v12和v13会分别替换为其指令的参数v22和v9。而v12和v13这两条指令如果在其他地方都没有引用,它将变成死代码,会在后续的死代码删除优化(以后会写文章来讲解)中将其消除。
2024-04-11 16:50:12
1088
原创 线程局部存储(TLS)
(Thread Local Storage,TLS),是一种变量的存储方法,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。而熟知的全局变量,是所有线程都可以访问的,这样就不可避免需要锁来控制,增加了控制成本和代码复杂度。
2024-03-27 10:45:20
2466
原创 RISC-V Optimization Guide(笔记)
使用lui/addiw将立即数加载至寄存器,当立即数低12位的最高位为1时,需要特殊处理,提前补值0x800。
2024-03-12 19:35:44
2307
原创 Go语言圣经总结
如果没有足够的增长空间的话,appendInt函数则会先分配一个足够大(2*len(old_slice),这个扩容算法因不同语言不同)的slice用于保存新的结果,先将输入的x复制到新的空间,然后添加y元素。当调用一个函数的时候,函数的每个调用参数将会被赋值给函数内部的参数变量,所以函数参数变量接收的是一个复制的副本,并不是原始调用的变量。因为函数参数传递的机制导致传递大的数组类型将是低效的,并且对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。
2024-01-18 14:37:01
1980
原创 DWARF(二):常见section详解
中源代码行号与机器码指令地址之间的映射关系,要先读取其中的内容,再按照规则和方法解析读取到的内容,最终就可以得到如上面代码段所示的内容。节中声明了很多中不同的Dwarf类型组合(我们可以想象为C语言中的结构声明,而这些类型都是DWARF格式约定好的类型),然后在。段包含了程序中的行号信息,通过读取、解析该段的内容,我们就能知道源代码行号与机器码指令地址之间的映射关系。节中的那个类型,也就是说明自己是那个结构的实例。这两个节是天生在一起的两个节,它们是一个“实例和类型”的关系,也就是。节中的一个结构的实例。
2023-12-22 17:12:17
2559
原创 Go Test测试总结
模糊测试的方式,运行 Fuzz 测试,默认情况下不进行模糊测试。指定时,命令行参数必须与主模块中的一个包完全匹配,而正则表达式必须与该包中的一个模糊测试完全匹配 ,模糊测试将在常规测试、基准测试、其他模糊测试的种子语料库和 Example 完成后进行。包用于调用相应的测试函数,接着构建并运行、报告测试结果,最后清理测试中生成的临时文件。:列出所有符合正则表达式的顶层测试,不会运行任何测试。本地目录模式,即直接运行当前目录下的包,即。,测试该目录下的所有包,在该模式下,包列表模式,运行指定包下的测试,
2023-09-20 11:03:11
2642
2
原创 LuaJIT:Bytecode结构布局
假设现在有两个寄存器PC和INS,PC存放Bytecode的地址,INS存放PC对应地址处的Bytecode值,则可以利用以下伪代码对Bytecode访问,其中。中的汇编代码在预处理阶段变成二进制机器码,然后再使用内存映射将机器码映射到虚拟内存,等运行时便可以直接运行被映射的二进制代码。中查找相应的函数指针,再执行相应的函数。等解释执行的逻辑完成,开始让PC指向下一条Bytecode指令,重复同样的过程。运行时被映射的二进制代码的地址(函数指针),存放在。类型的动态数组中,数组的初始。
2023-06-15 17:25:58
4328
5
原创 《编译器设计》第十四章——寄存器分配
在程序中的每个位置上,寄存器分配器会确定哪些值将位于寄存器中,哪个寄存器将容纳哪些值。如果分配器无法将某个值在其整个生命周期均保持在寄存器中,那么在其生命周期的部分或全部时间,该值必须存储到内存中。分配器可能会将一个值逐出到内存,因为代码包含的活跃值数据超出了目标机寄存器集合的容量。另外,在一个值各次使用之间的间歇,它可能被保存到内存中,因为分配器无法证明它能够安全地驻留在寄存器里。
2023-05-23 17:25:16
4914
原创 使用Makefile笔记总结
GNU的make很强大,它具有一些隐晦规则,可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个.o文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。只要make看到一个.o文件,它就会自动的把.c和.h文件加在依赖关系中,如make找到一个print.o,那么print.c和print.h,就会是print.o的依赖文件,并且 cc -c print.c 也会被推导出来。
2023-05-23 17:16:55
1710
原创 计算机体系结构基础(七):多核处理器结构
从20世纪90年代后期开始,随着半导体工艺的发展,单芯片上晶体管数目大幅增多,多核处理器得到了很好的发展。多核处理器(Multicore Processor)在单芯片上集成多个处理器核,也称为单片多处理器(Chip Multi-Processor,简称CMP),通过聚合芯片上的多个处理器核的计算能力来提高应用程序执行性能。各处理器核并行执行线程(或者进程)发出读/写(load/store)访存指令,这些访问指令的执行次序如何约定,使得应用程序员可以利用这些约定来推理程序的执行结果。片上Cache如何组织?
2023-04-01 20:07:17
5479
原创 使用Shell笔记总结
6、使用readonly可以将变量定义为只读变量,只读变量就是一个常量。unset 命令可以删除变量,变量被删除后不能再次使用,不能删除只读变量。原生bash不支持简单的数学运算,但是可以通过其他命令来实现,例如 awk 和 expr,expr 最常用。Bash Shell 只支持一维数组(不支持多维数组),初始化时不需要定义数组大小,数组元素的下标由 0 开始。等,可以保有其符号代表的特性,即可以有变量、转移字符;3、在一串指令的执行中,还需要藉由其他额外的指令所提供的信息时,可以使用反单引号。
2023-04-01 17:33:59
2313
原创 LuaJIT:栈帧布局(stack frames layout)
但是用堆内存代替寄存器和运行时栈在虚拟机层面的访问时效是相同(都是访问内存),所以我们可以效仿没有寄存器文件的龙芯处理器,在LuaJIT虚拟机中去除寄存器这一概念,只保留LuaJIT的运行时栈,LuaJIT也正是这么做的。主调函数调用被调函数之前,会先把被调函数的stack结构布置好,函数对象、参数放入指定位置,并在stack上留下存放PC的空间(这个PC以后再介绍吧,其实就是放函数调用返回后,将要执行的指令的)。,将slot 1中的值和slot 0号中的值相加,结果存放在slot 1号中。
2023-03-20 11:39:45
1089
原创 LuaJIT:常量池(constant array)
Lua source中出现的常量依次是131072、262144、1310721、131072,它们均为const number,所以在解析时将131072放在了索引0处,262144放在了索引1处,1310721放在了索引2处,解析到第二个131072时,发现该常量已经出现过且在索引0处,此时只需要将0放在操作数D上即可(中Node结构体,val和上面说过的TValue数组的索引对应,作为常量表slot,key则是常量值或字符串的sid,next存放下一个元素的地址。HashTable节点类型的定义是。
2023-03-17 13:37:01
970
原创 位运算实战技巧总结
两个负数相加可能会产生负溢出,负溢出结果会变为正数。如果两个数x和y符号相同,符号位异或结果为0;符号不同,符号位异或结果为1。如果一个正数是2的次幂,则这个数的二进制表示中只含有一个1。循环消去,当最后x值为0时,便可以求出二进制中1的个数。会找到第一个大于x的数,且它正好是n的整数倍。则是先对b按位取反,再将结果与a按位与运算。是最高权重,如果x,y的和数据溢出,则和模。运算的二进位结果,相异为1,相同为0。设整数n类型为int_8,值为3,则。成立,n为偶数,否则为奇数。比x,y中的任何一个值都小。
2023-02-15 18:11:24
1207
原创 《编译器设计》第十三章——指令调度
对程序块或过程中的操作进行排序以有效利用处理器资源的任务称为指令调度调度器的输入是由目标机汇编语言操作组成的一个部分有序的列表,输出是同一列表的一个有序版本。一组指令的执行时间严重依赖于其执行顺序,指令调度会重排一个过程中的各个指令,使每个周期执行尽可能多的指令,以改进其运行时间。对于整数加法或减法是1个周期;对于整数乘法或浮点加减法是3个周期;对于浮点乘法是5个周期;对于浮点除法是12-18个周期;对于整数除法是20-40周期。
2022-12-29 11:07:26
4020
原创 LuaJIT:SSA IR介绍
LuaJIT资料挺少,可以一手学习的基本上只有官方文档:http://wiki.luajit.org/SSA-IR-2.0,但是目前这个文档的介绍被作者删了。在文档被删之前,我按照自己的理解,对官网上的部分内容做了记录。等后面我想优化了,再把这一块内容好好优化一些,毕竟写这边文章的时候,我刚接触LuaJIT,对LuaJIT掌握的还
2022-12-09 16:47:46
1550
原创 LuaJIT:Bytecodes介绍
关于Bytecode介绍的官方文档:[http://wiki.luajit.org/Bytecode-2.0](http://wiki.luajit.org/Bytecode-2.0)但是最近发现作者将Bytecode和SSA IR的介绍的文档删除了,也不知道是为什么。虽然那些文档写的不太详细,除非看它们在LuaJIT中的具体实现代码,否则很难看懂。这篇文章我尽量说详细点,会结合Bytecode在代码中的具体定义,希望对需要的同学有所帮助。在LuaJIT的源码中,关于Bytecode的指令格式的定义
2022-12-09 16:28:07
2642
原创 《编译器设计》第十二章——指令选择
指令选择(instruction selection),将编译器的IR映射到目标ISA,这实际上是一个模式匹配问题,其复杂性源自常见的ISA为(即使是简单的)操作提供的大量备选实现方案。在其最简单的形式下,编译器可以为每个IR操作提供一个目标ISA操作序列。由此形成的指令选择器提供了一个类似模板的展开,最终会生成正确的代码。遗憾的是,这种代码对目标机资源的利用比较糟糕。更好的方法会对每个IR操作考虑许多可能的候选代码序列,并从中选择预期代价最低的代码序列。本章阐述指令选择的两种方法:一种基于树模式匹配。
2022-12-09 16:19:44
2767
1
原创 《编译器设计》第十一章——标量优化
所谓的标量优化,指的是单个控制线程下代码的优化。大多数优化器都被构建为一系列处理趟(pass),如下图所示。每趟以IR形式的代码作为其输入,以重写后的IR代码版本作出其输出。这种结构将实现划分为若干小片段,从而避免了大型单块程序引起的部分复杂性。这允许独立地构建并测试各个处理趟,简化了开发、测试和维护。这建立的方法颇为自然,使编译器能够提供不同的优化级别,每个级别规定了一组需要运行的处理趟。趟结构使编译器编写者能够多次运行某些趟(如果需要的话)。
2022-11-24 11:20:13
1784
原创 使用readelf和objdump查看ELF常见段
就是就是将几个输入目标文件加工后合并成一个输出文件,整个链接过程分两步:空间与地址分配、符号解析与重定位。的3大步骤:启动动态链接器本身 → 装载所有需要的共享对象 → 重定位和初始化。中对这些段又做了详细的解释,用到的时候只需查询即可。中给出了其中常见的段,解释了每个段的含义和用途。查看数据段相关的值需要。查看代码段相关的内容只需要。将16进制打印出来;
2022-11-02 13:14:56
13441
原创 《编译器设计》第十章——数据流分析
数据流分析是用于编译时程序分析的经典技术,它使编译器能够推断程序中的值在运行时的流动。在简单的情况下,静态分析可以生成精确的结果,此时编译器能够确切地知道在代码执行时到底将发生什么。如果编译器能够推导出精确信息,那么它可以将表达式或函数的运行时求值操作替换为对(编译时预计算)结果的立即数加载操作。另一方面,如果代码从任何外部来源读取值、涉及(即使很少的)控制流,或者遇到具有歧义的内存引用(指针、数组引用或引用调用参数),那么静态分析会变得困难得多,而分析的结果也会变得不那么精确。
2022-10-25 14:12:34
5449
原创 计算机体系结构基础(六):计算机组成原理和结构
现代计算机都采用存储程序结构,又称为冯·诺依曼结构,是1945年匈牙利籍数学家冯·诺依曼受宾夕法尼亚大学研制的ENIAC计算机结构的启发提出的,是世界上第一个完整的计算机体系结构。冯·诺依曼结构的主要特点是:冯·诺依曼计算机的工作原理如下图5.1所示。运算器是计算机中负责计算(包括算术计算和逻辑计算等)的部件。运算器包括算术和逻辑运算部件(Arithmetic Logic Units,简称ALU)、移位部件、浮点运算部件(Floating Point Units,简称FPU)、向量运算部件、寄存器等。其中
2022-10-10 21:08:27
5177
1
原创 计算机体系结构基础(五):常见上下文切换场景
函数调用是用户主动发起的指令流和上下文改变。普通的转移指令只改变指令流不改变上下文,函数调用则通过ABI约定实现了一定的上下文变化。函数调用通常伴随着栈帧的变化,此外部分寄存器也会发生变化。根据ABI的约定,像这样约定由被调用者保存(Callee Save)的寄存器在函数调用前后保持不变,而通用暂存器、参数寄存器等则不保证维持调用前的值。不同指令系统实现函数调用的方式有所不同。LoongArch采用比较典型的RISC做法,硬件仅仅提供一个机制(bl或者jirl指令),用于在改变指令流的同时保存一个返回地址到
2022-09-28 18:16:12
773
原创 《编译器设计》第九章——优化简介
代码优化的目标是在编译时发现有关程序运行时行为的信息,并利用该信息来改进编译器生成的代码。改进可能有许多种形式。优化最常见的目标是提高编译后代码的运行速度。但对于某些应用程序来说,编译后代码的长度要比其执行速度更重要。例如,考虑某个将烧录到只读存储器的应用程序,其代码长度会影响整个系统的成本。优化的其他目标包括降低执行的能耗、提高代码对实时事件的响应、降低对内存的总访问量、优化寄存器的使用等。有两个关于优化的比较经典的例子,后面补上。
2022-09-17 15:09:29
7607
原创 《编译器设计》第八章——代码形式
如果允许用户获得结构数组中数组元素的地址,那么编译骈在内存中对数据进行布局时,结构数组的布局将呈现为多个布局相同、在内存中连续出现的结构实例。如果编译器将一个静态值保持在寄存器中,那么在过程中第一次使用该值之前,必须将其从内存加载到寄存器,在离开过程之前(退出该过程时,或在过程内部调用其他过程时),必须将其写回到内存。,命名值a,未命名值b + 1),编译器都必须确定在何处存储这些值:是在内存中还是在寄存器中,不管是哪种情况,都要给出具体的位置。、最后一维的下界、B中一个元素的长度,来计算一个偏移量。
2022-09-09 09:19:37
1346
原创 《链接装载与库》第十二章——系统调用与API
在现代的操作系统里,程序运行的时候,本身是没有权利访问多少系统资源的。由于系统有限的资源有可能被多个不同的应用程序同时访问,因此,如果不加以保护,那么各个应用程序难免产生冲突。所以现代操作系统都将可能产生冲突的系统资源给保护起来,阻止应用程序直接访问。这些系统资源包括文件、网络、IO、各种设备等。举个例子,无论在Windows下还是Linux下,程序员都没有机会擅自去访问硬盘的某扇区上面的数据,而必须通过文件系统;也不能擅自修改任意文件,所有的这些操作都必须经由操作系统所规定的方式来进行,比如我们使用。
2022-09-05 17:02:02
1213
原创 《链接装载与库》第十一章——运行库
除了之前的14个头文件,剩下的15个头文件(C89标准)为:assert.h、ctype.h、errno.h、float.h、limits.h、locale.h、math.h、setjmp.h、signal.h、stdarg.h、stddef.h、stdio.h、stdlib.h、string.h、time.h。glibc的启动过程在不同的情况下差别很大,比如静态的glibc和动态的glibc的差别,glibc用于可执行文件和用于共享库的差别,这样的差别可以组合出4种情况,这里只选取最简单的。
2022-09-03 16:11:17
1290
原创 《链接装载与库》第八章——Linux共享库组织
这样保证了所有的以SO-NAME为名的软链接都指向系统中最新版的共享库。当共享库进行升级的时候,如果只是进行增量升级,即保持主版本号不变,只改变次版本号或发布版本号,那么我们可以直接将新版的共享库替换掉旧版,并且修改SO-NAME的软链接指向新版本共享库,即可实现升级;由于全局符号介入这个机制的存在,LD_PRELOAD里面指定的共享库或目标文件中的全局符号就会覆盖后面加载的同名全局符号,这使得我们可以很方便地做到改写标准C库中的某个或某几个函数而不影响其它函数,对于程序的调试或测试非常有用。
2022-09-01 19:50:13
1095
原创 Vim使用(一)——常用技巧及快捷键总结
普通用户的vim配置在。```shellset mouse-=asyntax on "语法高亮set nocompatible "去除VI一致性,必须要添加set nu "显示行号set autowrite "自动保存set ruler "打开状态栏标尺set cursorline "突出显示当前行set smartindent "开
2022-08-18 21:07:11
1812
原创 《链接装载与库》第七章——动态链接
将程序的模块互相分割开,不让它们静态地链接在一起;即不对那些组成程序的目标文件进行链接,等到程序运行时再链接。有如Program1和Program2两个程序,并假定保留了Program1.o、Program2.o和Lib.o三个文件。当我们需要运行Program1这个程序时,系统首先加载 Program1.o,而系统发现Program1.o中用到了Lib.o,即Program1.o依赖于Lib.o,此时系统加载Lib.o,并将其加载至内存。所有需要的目标文件加载完毕后,若依赖关系满足,即所有的依赖文件都在磁
2022-08-16 08:49:39
6210
2
原创 《链接装载与库》第六章——可执行文件的装载与进程
当linux系统在bash下输入一个命令执行ELF的时候。首先在用户层面,bash进程会调用fork系统调用创建一个新的进程,然后新的进程调用execve系统调用执行指定的ELF文件。原先的bash进程继续返回等待刚才启动的新进程结束,然后继续等待用户输入命令。在进入execve系统调用之后,linux内核就开始进行了真正的装载工作。httpshttpshttpshttpshttpshttps。...............
2022-07-27 13:16:09
1126
空空如也
空空如也
TA创建的收藏夹 TA关注的收藏夹
TA关注的人
RSS订阅