基于栈与基于寄存器的指令集架构

2023-11-08

用C的语法来写这么一个语句:
C代码   收藏代码
  1. a = b + c;  

如果把它变成这种形式:
add a, b, c
那看起来就更像机器指令了,对吧?这种就是所谓“三地址指令”(3-address instruction),一般形式为:
op dest, src1, src2
许多操作都是二元运算+赋值。三地址指令正好可以指定两个源和一个目标,能非常灵活的支持二元操作与赋值的组合。ARM处理器的主要指令集就是三地址形式的。

C里要是这样写的话:
C代码   收藏代码
  1. a += b;  

变成:
add a, b
这就是所谓“二地址指令”,一般形式为:
op dest, src
它要支持二元操作,就只能把其中一个源同时也作为目标。上面的add a, b在执行过后,就会破坏a原有的值,而b的值保持不变。x86系列的处理器就是二地址形式的。

上面提到的三地址与二地址形式的指令集,一般就是通过“基于寄存器的架构”来实现的。例如典型的RISC架构会要求除load和store以外,其它用于运算的指令的源与目标都要是寄存器。

显然,指令集可以是任意“n地址”的,n属于自然数。那么一地址形式的指令集是怎样的呢?
想像一下这样一组指令序列:
add 5
sub 3
这只指定了操作的源,那目标是什么?一般来说,这种运算的目标是被称为“累加器”(accumulator)的专用寄存器,所有运算都靠更新累加器的状态来完成。那么上面两条指令用C来写就类似:
C代码   收藏代码
  1. acc += 5;  
  2. acc -= 3;  

只不过acc是“隐藏”的目标。基于累加器的架构近来比较少见了,在很老的机器上繁荣过一段时间。

那“n地址”的n如果是0的话呢?
看这样一段Java字节码:
Java bytecode代码   收藏代码
  1. iconst_1  
  2. iconst_2  
  3. iadd  
  4. istore_0  

注意那个iadd(表示整型加法)指令并没有任何参数。连源都无法指定了,零地址指令有什么用??
零地址意味着源与目标都是隐含参数,其实现依赖于一种常见的数据结构——没错,就是栈。上面的iconst_1、iconst_2两条指令,分别向一个叫做“求值栈”(evaluation stack,也叫做operand stack“操作数栈”或者expression stack“表达式栈”)的地方压入整型常量1、2。iadd指令则从求值栈顶弹出2个值,将值相加,然后把结果压回到栈顶。istore_0指令从求值栈顶弹出一个值,并将值保存到局部变量区的第一个位置(slot 0)。
零地址形式的指令集一般就是通过“基于栈的架构”来实现的。请一定要注意,这个栈是指“求值栈”,而不是与系统调用栈(system call stack,或者就叫system stack)。千万别弄混了。有些虚拟机把求值栈实现在系统调用栈上,但两者概念上不是一个东西。

由于指令的源与目标都是隐含的,零地址指令的“密度”可以非常高——可以用更少空间放下更多条指令。因此在空间紧缺的环境中,零地址指令是种可取的设计。但零地址指令要完成一件事情,一般会比二地址或者三地址指令许多更多条指令。上面Java字节码做的加法,如果用x86指令两条就能完成了:
X86 asm代码   收藏代码
  1. mov  eax, 1  
  2. add  eax, 2  

(好吧我犯规了,istore_0对应的保存我没写。但假如局部变量比较少的话也不必把EAX的值保存(“溢出”,register spilling)到调用栈上,就这样吧 =_=
其实就算把结果保存到栈上也就是多一条指令而已……)

一些比较老的解释器,例如 CRuby在1.9引入 YARV作为新的VM之前的解释器,还有SquirrleFish之前的老JavaScriptCore,它们内部是树遍历式解释器;解释器递归遍历树,树的每个节点的操作依赖于解释其各个子节点返回的值。这种解释器里没有所谓的求值栈,也没有所谓的虚拟寄存器,所以不适合以“基于栈”或“基于寄存器”去描述。

而像V8那样直接编译JavaScript生成机器码,而不通过中间的字节码的中间表示的JavaScript引擎,它内部有虚拟寄存器的概念,但那只是普通native编译器的正常组成部分。我觉得也不应该用“基于栈”或“基于寄存器”去描述它。
V8在内部也用了“求值栈”(在V8里具体叫“表达式栈”)的概念来简化生成代码的过程,使用所谓“虚拟栈帧”来记录局部变量与求值栈的状态;但在真正生成代码的时候会做窥孔优化,消除冗余的push/pop,将许多对求值栈的操作转变为对寄存器的操作,以此提高代码质量。于是最终生成出来的代码看起来就不像是基于栈的代码了。

关于JavaScript引擎的实现方式,下文会再提到。


基于栈与基于寄存器架构的VM,用哪个好?

如果是要模拟现有的处理器,那没什么可选的,原本处理器采用了什么架构就只能以它为源。但HLL VM的架构通常可以自由构造,有很大的选择余地。为什么许多主流HLL VM,诸如JVM、CLI、CPython、CRuby 1.9等,都采用了基于栈的架构呢?我觉得这有三个主要原因:

·实现简单
由于指令中不必显式指定源与目标,VM可以设计得很简单,不必考虑为临时变量分配空间的问题,求值过程中的临时数据存储都让求值栈包办就行。
更新:回帖中cscript指出了这句不太准确,应该是针对基于栈架构的指令集生成代码的编译器更容易实现,而不是VM更容易实现。

·该VM是为某类资源非常匮乏的硬件而设计的
这类硬件的存储器可能很小,每一字节的资源都要节省。零地址指令比其它形式的指令更紧凑,所以是个自然的选择。

·考虑到可移植性
处理器的特性各个不同:典型的CISC处理器的通用寄存器数量很少,例如32位的 x86就只有8个32位通用寄存器(如果不算EBP和ESP那就是6个,现在一般都算上);典型的RISC处理器的各种寄存器数量多一些,例如 ARM有16个32位通用寄存器,Sun的 SPARC在一个寄存器窗口里则有24个通用寄存器(8 in,8 local,8 out)。
假如一个VM采用基于寄存器的架构(它接受的指令集大概就是二地址或者三地址形式的),为了高效执行,一般会希望能把源架构中的寄存器映射到实际机器上寄存器上。但是VM里有些很重要的辅助数据会经常被访问,例如一些VM会保存源指令序列的程序计数器(program counter,PC),为了效率,这些数据也得放在实际机器的寄存器里。如果源架构中寄存器的数量跟实际机器的一样,或者前者比后者更多,那源架构的寄存器就没办法都映射到实际机器的寄存器上;这样VM实现起来比较麻烦,与能够全部映射相比效率也会大打折扣。
如果一个VM采用基于栈的架构,则无论在怎样的实际机器上,都很好实现——它的源架构里没有任何通用寄存器,所以实现VM时可以比较自由的分配实际机器的寄存器。于是这样的VM可移植性就比较高。作为优化,基于栈的VM可以用编译方式实现,“求值栈”实际上也可以由编译器映射到寄存器上,减轻数据移动的开销。

回到主题,基于栈与基于寄存器的架构,谁更快?看看现在的实际处理器,大多都是基于寄存器的架构,从侧面反映出它比基于栈的架构更优秀。
而对于VM来说,源架构的求值栈或者寄存器都可能是用实际机器的内存来模拟的,所以性能特性与实际硬件又有点不同。一般认为基于寄存器的架构对VM来说也是更快的,原因是:虽然零地址指令更紧凑,但完成操作需要更多的load/store指令,也意味着更多的指令分派(instruction dispatch)次数与内存访问次数;访问内存是执行速度的一个重要瓶颈,二地址或三地址指令虽然每条指令占的空间较多,但总体来说可以用更少的指令完成操作,指令分派与内存访问次数都较少。

这方面有篇被引用得很多的论文讲得比较清楚,Virtual Machine Showdown: Stack Versus Registers,是在VEE 2005发表的。VEE是Virtual Execution Environment的缩写,是ACM下SIGPLAN组织的一个会议,专门研讨虚拟机的设计与实现的。可以去找找这个会议往年的论文,很多都值得读


基于栈与基于寄存器架构的VM的一组图解

要是拿两个分别实现了基于栈与基于寄存器架构、但没有直接联系的VM来对比,效果或许不会太好。现在恰巧有两者有紧密联系的例子——JVM与Dalvik VM。JVM的字节码主要是零地址形式的,概念上说JVM是基于栈的架构。Google Android平台上的应用程序的主要开发语言是Java,通过其中的Dalvik VM来运行Java程序。为了能正确实现语义,Dalvik VM的许多设计都考虑到与JVM的兼容性;但它却采用了基于寄存器的架构,其字节码主要是二地址/三地址混合形式的,乍一看可能让人纳闷。考虑到Android有明确的目标:面向移动设备,特别是最初要对ARM提供良好的支持。ARM9有16个32位通用寄存器,Dalvik VM的架构也常用16个虚拟寄存器(一样多……没办法把虚拟寄存器全部直接映射到硬件寄存器上了);这样Dalvik VM就不用太顾虑可移植性的问题,优先考虑在ARM9上以高效的方式实现,发挥基于寄存器架构的优势。
Dalvik VM的主要设计者Dan Bornstein在Google I/O 2008上做过一个关于Dalvik内部实现的演讲;同一演讲也在Google Developer Day 2008 China和Japan等会议上重复过。这个演讲中Dan特别提到了Dalvik VM与JVM在字节码设计上的区别,指出Dalvik VM的字节码可以用更少指令条数、更少内存访问次数来完成操作。(看不到YouTube的请自行想办法)

眼见为实。要自己动手感受一下该例子,请先确保已经正确安装JDK 6,并从官网获取Android SDK 1.6R1。连不上官网的也请自己想办法。

创建Demo.java文件,内容为:

Java代码   收藏代码
  1. public class Demo {  
  2.     public static void foo() {  
  3.         int a = 1;  
  4.         int b = 2;  
  5.         int c = (a + b) * 5;  
  6.     }  
  7. }  

通过javac编译,得到Demo.class。通过javap可以看到foo()方法的字节码是:
Java bytecode代码   收藏代码
  1. 0:  iconst_1  
  2. 1:  istore_0  
  3. 2:  iconst_2  
  4. 3:  istore_1  
  5. 4:  iload_0  
  6. 5:  iload_1  
  7. 6:  iadd  
  8. 7:  iconst_5  
  9. 8:  imul  
  10. 9:  istore_2  
  11. 10: return  


接着用Android SDK里platforms\android-1.6\tools目录中的dx工具将Demo.class转换为dex格式。转换时可以直接以文本形式dump出dex文件的内容。使用下面的命令:
Command prompt代码   收藏代码
  1. dx --dex --verbose --dump-to=Demo.dex.txt --dump-method=Demo.foo --verbose-dump Demo.class  

可以看到foo()方法的字节码是:
Dalvik bytecode代码   收藏代码
  1. 0000: const/4       v0, #int 1 // #1  
  2. 0001: const/4       v1, #int 2 // #2  
  3. 0002: add-int/2addr v0, v1  
  4. 0003: mul-int/lit8  v0, v0, #int 5 // #05  
  5. 0005: return-void  

(原本的输出里还有些code-address、local-snapshot等,那些不是字节码的部分,可以忽略。)

让我们看看两个版本在概念上是如何工作的。
JVM:

(图中数字均以十六进制表示。其中字节码的一列表示的是字节码指令的实际数值,后面跟着的助记符则是其对应的文字形式。标记为红色的值是相对上一条指令的执行状态有所更新的值。下同)
说明:Java字节码以1字节为单元。上面代码中有11条指令,每条都只占1单元,共11单元==11字节。
程序计数器是用于记录程序当前执行的位置用的。对Java程序来说,每个线程都有自己的PC。PC以字节为单位记录当前运行位置里方法开头的偏移量。
每个线程都有一个Java栈,用于记录Java方法调用的“活动记录”(activation record)。Java栈以帧(frame)为单位线程的运行状态,每调用一个方法就会分配一个新的栈帧压入Java栈上,每从一个方法返回则弹出并撤销相应的栈帧。
每个栈帧包括局部变量区、求值栈(JVM规范中将其称为“操作数栈”)和其它一些信息。局部变量区用于存储方法的参数与局部变量,其中参数按源码中从左到右顺序保存在局部变量区开头的几个slot。求值栈用于保存求值的中间结果和调用别的方法的参数等。两者都以字长(32位的字)为单位,每个slot可以保存byte、short、char、int、float、reference和returnAddress等长度小于或等于32位的类型的数据;相邻两项可用于保存long和double类型的数据。每个方法所需要的局部变量区与求值栈大小都能够在编译时确定,并且记录在.class文件里。
在上面的例子中,Demo.foo()方法所需要的局部变量区大小为3个slot,需要的求值栈大小为2个slot。Java源码的a、b、c分别被分配到局部变量区的slot 0、slot 1和slot 2。可以观察到Java字节码是如何指示JVM将数据压入或弹出栈,以及数据是如何在栈与局部变量区之前流动的;可以看到数据移动的次数特别多。动画里可能不太明显,iadd和imul指令都是要从求值栈弹出两个值运算,再把结果压回到栈上的;光这样一条指令就有3次概念上的数据移动了。

对了,想提醒一下:Java的局部变量区并不需要把某个局部变量固定分配在某个slot里;不仅如此,在一个方法内某个slot甚至可能保存不同类型的数据。如何分配slot是编译器的自由。从类型安全的角度看,只要对某个slot的一次load的类型与最近一次对它的store的类型匹配,JVM的字节码校验器就不会抱怨。以后再找时间写写这方面。

Dalvik VM:

说明:Dalvik字节码以16位为单元(或许叫“双字节码”更准确 =_=|||)。上面代码中有5条指令,其中mul-int/lit8指令占2单元,其余每条都只占1单元,共6单元==12字节。
与JVM相似,在Dalvik VM中每个线程都有自己的PC和调用栈,方法调用的活动记录以帧为单位保存在调用栈上。PC记录的是以16位为单位的偏移量而不是以字节为单位的。
与JVM不同的是,Dalvik VM的栈帧中没有局部变量区与求值栈,取而代之的是一组虚拟寄存器。每个方法被调用时都会得到自己的一组虚拟寄存器。常用v0-v15这16个,也有少数指令可以访问v0-v255范围内的256个虚拟寄存器。与JVM相同的是,每个方法所需要的虚拟寄存器个数都能够在编译时确定,并且记录在.dex文件里;每个寄存器都是字长(32位),相邻的一对寄存器可用于保存64位数据。方法的参数按源码中从左到右的顺序保存在末尾的几个虚拟寄存器里。
与JVM版相比,可以发现Dalvik版程序的指令数明显减少了,数据移动次数也明显减少了,用于保存临时结果的存储单元也减少了。

你可能会抱怨:上面两个版本的代码明明不对应:JVM版到return前完好持有a、b、c三个变量的值;而Dalvik版到return-void前只持有b与c的值(分别位于v0与v1),a的值被刷掉了。
但注意到a与b的特征:它们都只在声明时接受过一次赋值,赋值的源是常量。这样就可以对它们应用 常量传播,将
Java代码   收藏代码
  1. int c = (a + b) * 5;  

替换为
Java代码   收藏代码
  1. int c = (1 + 2) * 5;  

然后可以再对c的初始化表达式应用常量折叠,进一步替换为:
Java代码   收藏代码
  1. int c = 15;  

把变量的每次状态更新(包括初始赋值在内)称为变量的一次“定义”(definition),把每次访问变量(从变量读取值)称为变量的一次“使用”(use),则可以把代码整理为“使用-定义链”(简称UD链, use-define chain)。显然,一个变量的某次定义要被使用过才有意义。上面的例子经过常量传播与折叠后,我们可以分析得知变量a、b、c都只被定义而没有被使用。于是它们的定义就成为了无用代码(dead code),可以安全的被消除。
上面一段的分析用一句话描述就是:由于foo()里没有产生外部可见的副作用,所以foo()的整个方法体都可以被优化为空。经过dx工具处理后,Dalvik版程序相对JVM版确实是稍微优化了一些,不过没有影响程序的语义,程序的正确性是没问题的。这是其一。

其二是Dalvik版代码只要多分配一个虚拟寄存器就能在return-void前同时持有a、b、c三个变量的值,指令几乎没有变化:
Dalvik bytecode代码   收藏代码
  1. 0000: const/4      v0, #int 1 // #1  
  2. 0001: const/4      v1, #int 2 // #2  
  3. 0002: add-int      v2, v0, v1  
  4. 0004: mul-int/lit8 v2, v2, #int 5 // #05  
  5. 0006: return-void  

这样比原先的版本多使用了一个虚拟寄存器,指令方面也多用了一个单元(add-int指令占2单元);但指令的条数没变,仍然是5条,数据移动的次数也没变。

题外话1:Dalvik VM是基于寄存器的,x86也是基于寄存器的,但两者的“寄存器”却相当不同:前者的寄存器是每个方法被调用时都有自己一组私有的,后者的寄存器则是全局的。也就是说,Dalvik VM字节码中不用担心保护寄存器的问题,某个方法在调用了别的方法返回过来后自己的寄存器的值肯定跟调用前一样。而x86程序在调用函数时要考虑清楚 calling convention,调用方在调用前要不要保护某些寄存器的当前状态,还是说被调用方会处理好这些问题,麻烦事不少。Dalvik VM这种虚拟寄存器让人想起一些实际处理器的“寄存器窗口”,例如SPARC的 Register Windows也是保证每个函数都觉得自己有“私有的一组寄存器”,减轻了在代码里处理寄存器保护的麻烦——扔给硬件和操作系统解决了。 IA-64也有寄存器窗口的概念。

题外话2:Dalvik的.dex文件在未压缩状态下的体积通常比同等内容的.jar文件在deflate压缩后还要小。但光从字节码看,Java字节码几乎总是比Dalvik的小,那.dex文件的体积是从哪里来减出来的呢?这主要得益与.dex文件对常量池的压缩,一个.dex文件中所有类都共享常量池,使得相同的字符串、相同的数字常量等都只出现一次,自然能大大减小体积。相比之下,.jar文件中每个类都持有自己的常量池,诸如"Ljava/lang/Object;"这种常见的字符串会被重复多次。Sun自己也有进一步压缩JAR的工具,Pack200,对应的标准是 JSR 200。它的主要应用场景是作为JAR的网络传输格式,以更高的压缩比来减少文件传输时间。在 官方文档提到了Pack200所用到的压缩技巧,
JDK 5.0 Documentation 写道
Pack200 works most efficiently on Java class files. It uses several techniques to efficiently reduce the size of JAR files:
  • It merges and sorts the constant-pool data in the class files and co-locates them in the archive.
  • It removes redundant class attributes.
  • It stores internal data structures.
  • It use delta and variable length encoding.
  • It chooses optimum coding types for secondary compression.
可见.dex文件与Pack200采用了一些相似的减小体积的方法。很可惜目前还没有正式发布的JVM支持直接加载Pack200格式的归档,毕竟网络传输才是Pack200最初构想的应用场景。

再次提醒注意, 上面的描述是针对概念上的JVM与Dalvik VM,而不是针对它们的具体实现。实现VM时可以采用许多优化技巧去减少性能损失,使得实际的运行方式与概念中的不完全相符,只要最终的运行结果满足原本概念上的VM所实现的语义就行

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于栈与基于寄存器的指令集架构 的相关文章

随机推荐

  • Java-Redis缓存穿透,击穿,雪崩和布隆算法

    Java Redis缓存穿透 击穿 雪崩和布隆算法 1 缓存穿透概念 2 如何解决缓存穿透 3 什么是缓存击穿 4 什么是缓存雪崩 5 导致缓存雪崩的原因 6 缓存穿透 缓存击穿 缓存雪崩的区别 1 缓存穿透概念 当一个用户想要查询数据时
  • LRU算法java实现

    1 lru简介 LRU是Least Recently Used的缩写 即最近最少使用 常用于页面置换算法 是为虚拟页式存储管理服务的 即当一个数据最近一段时间没有被访问 未来被访问的概率也很小 当空间被占满后 最先淘汰最近最少使用的数据 2
  • Android动态来改变App桌面图标

    时不时的我们就会发现 一些我们常见的应用 比如某宝 某东 在一些特殊的日子中 比如双十一 元旦 为了迎合这样一个日子的气氛 在桌面的应用图标就会发生改变 其实对于这样的一个桌面图标更换 Android中为我们提供了AndroidManife
  • spring data jpa 关联查询返回自定义对象

    Override public List
  • Linux性能检测常用的10个基本命令

    1 uptime 该命令可以大致的看出计算机的整体负载情况 load average后的数字分别表示计算机在1min 5min 15min内的平均负载 2 dmesg tail 打印内核环形缓存区中的内容 可以用来查看一些错误 上面的例子中
  • vue3组件库搭建并且发布到npm保姆教程连载一

    前言 小时候的梦想是拥有一个自己的组件库 开玩笑哈 接触前端后 很多时候在npm install的时候 我在想我们安装的这些依赖发布者是如何将依赖发布到npm 并且可以让别人使用的 未知是让人害怕的 经过一系列学习和探索后 我也拥有了自己的
  • 【python数据挖掘课程】二十六.基于SnowNLP的豆瓣评论情感分析

    这是 Python数据挖掘课程 系列文章 前面很多文章都讲解了分类 聚类算法 而这篇文章主要讲解如何调用SnowNLP库实现情感分析 处理的对象是豆瓣 肖申克救赎 的评论文本 文章比较基础 希望对你有所帮助 提供些思路 也是自己教学的内容
  • 全国青少年电子信息智能创新大赛(决赛)python·模拟三卷,含答案解析

    全国青少年电子信息智能创新大赛 决赛 python 模拟三卷 一 程序题 第一题 描述 现有 n 个人依次围成一圈玩游戏 从第 1 个人开始报数 数到第 m 个人出局 然 后从出局的下一个人开始报数 数到第 m 个人又出局 如此反复到只剩下
  • Google分布式三篇论文---BigTable

    Google s BigTable 原理 翻译 题记 google 的成功除了一个个出色的创意外 还因为有 Jeff Dean 这样的软件架构天才 官方的 Google Reader blog 中有对BigTable 的解释 这是Googl
  • TensorRT(2):TensorRT的使用流程

    TensorRT系列传送门 不定期更新 深度框架 TensorRT 文章目录 一 在线加载caffe模型 序列化保存到本地 二 反序列化直接加载保存后的trt模型 以caffe分类模型为例 简单介绍TRT的使用流程 这里不涉及量化 就以fp
  • 测试的艺术:代码检查、走查与评审

    软件开发人员通常不会考虑的一种测试形式 人工测试 大多数人都以为 因为程序是为了供机器执行而编写的 那么也该由机器来对程序进行测试 这种想法是有问题的 人工测试方法在暴露错误方面是很有成效的 实际上 大多数的软件项目都应使用到一下的人工测试
  • 详解Shell 脚本中 “$” 符号的多种用法

    通常情况下 在工作中用的最多的有如下几项 1 表示执行脚本传入参数的个数 2 表示执行脚本传入参数的列表 不包括 0 3 表示进程的id Shell本身的PID ProcessID 即脚本运行的当前 进程ID号 4 Shell最后运行的后台
  • 解决uni-toast被弹窗组件遮挡

    在App vue uni toast设置层级比popup高就行 uni toast z index 999999
  • 输入文本就可建模渲染了?!OpenAI祭出120亿参数魔法模型!

    转自 https new qq com omn 20210111 20210111A0CBRD00 html 2021刚刚开启 OpenAI又来放大招了 能写小说 哲学语录的GPT 3已经不足为奇 那就来一个多模态 图像版GPT 3 今天
  • 微信小程序事件和页面跳转

    一 页面跳转 1 非TabBar页面 一个小程序拥有多个页面 我们通过wx navigateTo进入一个新的页面 我们通过下边点击事件实现页面跳转进行代码实现及参考 wx navigateBack 回退到上一个页面 wx redirectT
  • 【单片机毕业设计】【mcuclub-310】红外遥控器

    设计简介 项目名 基于单片机的红外遥控器的设计 标准版 单片机 STC89C52 功能简介 1 利用红外发射电路 通过按不同的按键发送不同的数据值 2 利用红外接收电路 接收发送端发送的数据 3 通过数码管显示数据 资料预览 效果图 发送端
  • MiniGPT-4本地部署的实战方案

    大家好 我是herosunly 985院校硕士毕业 现担任算法研究员一职 热衷于机器学习算法研究与应用 曾获得阿里云天池比赛第一名 CCF比赛第二名 科大讯飞比赛第三名 拥有多项发明专利 对机器学习和深度学习拥有自己独到的见解 曾经辅导过若
  • Ubuntu22下OpenCV4.6.0+contrib模块编译安装

    目录 第一章 Ubuntu22下OpenCV4 6 0 contrib模块编译安装 第二章 ubuntu22下C kdevelop环境搭建 OpenCV示例 第三章 C 下OPENCV驱动调用海康GigE工业相机 文章目录 目录 Ubunt
  • K8S常用资源认识

    文章目录 一 Namespace 二 Pod 三 Label 四 Deployment 五 Service 一 Namespace namespace是kubernetes系统中的一种非常重要的资源 它的主要作用是用来实现多套环境的资源隔离
  • 基于栈与基于寄存器的指令集架构

    用C的语法来写这么一个语句 C代码 a b c 如果把它变成这种形式 add a b c 那看起来就更像机器指令了 对吧 这种就是所谓 三地址指令 3 address instruction 一般形式为 op dest src1 src2