《计算机组成与设计(ARM版)》读书笔记-第二章指令2
文章目录
- 2.9 人机交互
- 2.10 LEGv8中的宽立即数和地址的寻址
- 2.10.1 宽立即数
- 2.10.2 分支中的寻址
- 2.10.3 LEGv8寻址模式总结
- 2.10.4 机器语言解码
- 2.11 并行与指令:同步
- 2.12 翻译并启动程序
- 2.12.1 编译器
- 2.12.2 汇编器
- 2.12.3 链接器
- 2.12.4 加载器
- 2.12.5 动态链接库
- 2.12.6 启动Java程序
- 2.13 综合实例:C排序程序
- 2.13.1 swap过程
- 1 为swap分配寄存器
- 2 为swap过程体生成代码
- 3 完整的swap过程
- 2.13.2 sort过程
- 1 为sort分配寄存器
- 2 为sort过程体生成代码
- 3 sort 中的过程调用
- 4 sort中的参数传递
- 5 在sort中保存寄存器
- 6 完整的sort过程
2.9 人机交互
计算机发明最初是用于处理数字的,但在进入商业应用时,计算机已经能够处理文字了。今天计算机基本都使用8位的字节来表示字符,并遵行美国信息交换标准代码(即ASCII码)。
注意,在ASCII码中,所有大写字母和对应小写字母的差均为32,这个规律可以用于快速检查和切换大小写。 另外一个有用的ASCII值是0,表示null,C语言常用它来标记字符串的结尾。
通过使用一系列指令可以从一个双字中提取出一个字节,所以load register 和 store register 足以完成字和字节的传输。然而,很多程序中经常要处理文本,因此LEGv8额外提供了字节移动指令。读字节指令LDURB
(load byte)从内存中读出一个字节,并将其放在一个寄存器最右边的8位。存字节指令STURB
(store byte)把一个寄存器最右边的8位(即一个字节)取出,并写到内存。这样,我们可以通过以下指令序列复制一个字节:
LDURB X9,[X0,#0] // 读取字节
STURB X9,[X1,#0] // 向目的地写 字节
字符通常被组合为字符数目可变的字符串。表示一个字符串有三种方法:(1) 保留字符串的第一个位置用于给出字符串的长度;(Java采用该种方法)(2)附加一个指明字符串长度的变量(如再结构体中)。(3)字符串最后的位置用一个字符来标识其结尾。C语言使用第三种方法,用一个值为零(ASCII码中的null)的字节来结束字符串。因此,在C语言中,字符串“Cal” 由4字节表示,由十进制分别表示为67,97,108,0.
例题
编译一个字符串复制过程,体会如何使用C语言的字符串
过程strcpy利用C语言中以null字节结束字符串的规定,将字符串y赋值给字符串x:
void strcpy( char x[],char y[]){size_t i;i=0;while((x[i]=y[i])!='\0') //赋值并且判断是否符合条件i+=1;}
请给出编译后的LEGv8汇编代码。
下面是在DS-5中 反汇编得到的结果,值得研究
strcpy0x0000000080001218: SUB sp, sp, #0x200x000000008000121C: STR x0, [sp, #0x8]0x0000000080001220: STR x1, [sp]0x0000000080001224: STR xzr, [sp, #0x18]0x0000000080001228: B 0x800012380x000000008000122C: LDR x0, [sp, #0x18]0x0000000080001230: ADD x0, x0, #0x10x0000000080001234: STR x0, [sp, #0x18]0x0000000080001238: LDR x1, [sp]0x000000008000123C: LDR x0, [sp, #0x18]0x0000000080001240: ADD x1, x1, x00x0000000080001244: LDR x2, [sp, #0x8]0x0000000080001248: LDR x0, [sp, #0x18]0x000000008000124C: ADD x0, x2, x00x0000000080001250: LDRB w1, [x1]0x0000000080001254: STRB w1, [x0]0x0000000080001258: LDRB w0, [x0]0x000000008000125C: CMP w0, #0x00x0000000080001260: B.NE 0x8000122c0x0000000080001264: NOP0x0000000080001268: ADD sp, sp, #0x200x000000008000126C: RET
答案
下面是基本的LEGv8代码片段。假设数组x和y的基址在X0和X1中,而i在X19中。strcpy调整栈指针,然后将保存寄存器X19保存在栈中。
strcpy:SUBI SP,SP,#8 //调整堆栈指针,为一个元素留出空地STUR X19, [SP,#0]//SAVE X19
Java中的字符和字符串
Java对字符采用Unicode编码,默认情况下,Unicode使用16位来表示一个字符。ASCII使用8位表示一个字符。
LEGv8指令集中的一些指令能够显示地读取和存储16位量,即半字。读半字指令LDURH
(load half) 从存储器中读出一个半字,然后将其放到寄存器的最右边16位。与读字节类似,读半字指令LDURH也将半字看作有符号苏,并进行符号扩展以填充寄存器中剩余的48位。存半字指令STRUH
(store half)将寄存器最右边的16位写入寄存器。 下面的指令可以复制半字
LDURH X19,[X0,#0]//从源地址读取半字(16bits)
STURH X9,[X1,#0]//向目的写入半字
下面哪种类型的变量存放1000 000 000占用的内存空间最大?
A C语言中的long long int
long long在win32中存在,长度为8个字节;定义为LONG64。
B C语言中的 string
C Java中的string
答案是C
参考:Java String 占用内存大小分析
2.10 LEGv8中的宽立即数和地址的寻址
2.10.1 宽立即数
2.10.2 分支中的寻址
LEGv8跳转指令(无条件分支指令)采用最简单的寻址方式,使用B型LEGv8指令格式,操作码为6位,其余为都是地址段。
B 10000 // go to location 10000(Decimal)
可以汇编成下面的格式(实际中更为复杂):
5 | 10000ten10000_{ten}10000ten |
---|---|
6位 | 26位 |
其中,跳转指令的操作码值为5,分支地址为10000ten10000_{ten}10000ten。
和跳转指令不同,条件分支指令除了分支地址之外还可以指定一个操作数。
因此:
CBNZ X19,Exit //go to Exit if X19≠0
被汇编成下面的指令,其中只有19位用于指定分支地址:
181 | Exit | 19 |
---|---|---|
8位 | 19位 | 5位 |
对于条件分支指令,这种格式叫作CB型。
如果程序的地址只能放在19位的字段中,这意味着没有程序能大于2192^{19}219,这在今天来说实在太小,因此是一种很不现实的选择。
另一种方法是指定一个寄存器,该寄存器的值用于和分支地址的偏移量相加以得到最终地址,这样分支指令的地址可按下面的方式计算:
程序计数器=寄存器内容+分支地址偏移量程序计数器=寄存器内容+分支地址偏移量程序计数器=寄存器内容+分支地址偏移量
这个求和结果允许程序的大小得到2642^{64}264,并且仍能使用条件分支,从而解决了分支地址大小的问题。但随之而来的问题是,使用哪个寄存器?
答案取决于条件分支是如何使用的。 条件分支在循环和if语句中都可以找到,它们倾向于转向附近的指令。例如,在SPEC基准测试程序中,大概一半的条件分支转移范围都在16条指令以内。因为程序计数器(PC)包含当前指令的地址,所以如果我们使用PC作为计算地址的寄存器,就可转移到距当前指令±218±2^{18}±218个字(1个字=32位)的地方。 几乎所有的循环和if语句都远远小于±218±2^{18}±218个字,因此PC是一个理想的选择。这种分支地址的寻址方式称为PC相对寻址(PC-relative addressing).
PC相对寻址:一种寻址方式,将PC和指令中的常数相加作为地址。
像近期大多数计算机一样,LEGv8对所有条件分支使用PC相对寻址,因为这些指令的跳转目标一般都比较接近分支指令本身。 另一方面,分支和链接(branch-and-link)指令引发的过程则并不一定总是靠近调用者,所以通常使用其他寻址方式。 因此,LEGv8 体系结构通过对分支指令以及分支和链接指令采用B型指令格式,为过程调用提供长地址。
因为LEGv8的所有指令都是4字节长,所以将PC相对寻址的地址设计成字地址(1个字=32位)而不是字节地址,从而可以扩展分支转移的范围。 通过将字段解释成相对字地址而不是相对字节地址,19位的地址字段所指示的转移范围扩大了4倍:当前PC±1MB。同样,分支指令的26位字段 也是字地址,即表示28位字节地址。
这里的1MB是怎么算出来的呢? 19位地址提供2182^{18}218种寻址方式,又因为是字地址,还需要乘以4倍(即222^222)所以 寻址大小为218×22=220=1MB2^{18}\times 2^2=2^{20} =1MB218×22=220=1MB
无条件分支(即跳转指令)也采用PC相对寻址,这意味着转移范围是当前PC值±128MB。
这里的128MB是怎么算出来的呢? 26位地址提供2252^{25}225种寻址方式,又因为是字地址,还需要乘以4倍(即222^222)所以 寻址大小为225×22=227=220×27=128MB2^{25}\times 2^2=2^{27}=2^{20} \times 2^7 =128MB225×22=227=220×27=128MB
2.10.3 LEGv8寻址模式总结
2.10.4 机器语言解码
小测验:
LEGv8中条件分支的地址范围多大(K=1024)?
答案: 分支前后大约1024K(即1MB)的地址范围
LEGv8中条件分支的指令格式为
opcode | Address | Rt |
---|---|---|
8位 | 19位 | 5位 |
其中19位用于指定分支地址。
这里的1MB是怎么算出来的呢? 19位地址提供2182^{18}218种寻址方式,又因为是字地址,还需要乘以4倍(即222^222)所以 寻址大小为218×22=220=1MB2^{18}\times 2^2=2^{20} =1MB218×22=220=1MB
LEGv8中跳转和跳转链接指令的地址范围(M=1024K)是多大?
答案:分支前后大约128M的地址范围
opcode | addressaddressaddress |
---|---|
6位 | 26位 |
这里的128MB是怎么算出来的呢? 26位地址提供2252^{25}225种寻址方式,又因为是字地址,还需要乘以4倍(即222^222)所以 寻址大小为225×22=227=220×27=128MB2^{25}\times 2^2=2^{27}=2^{20} \times 2^7 =128MB225×22=227=220×27=128MB
另外关于MIPS指令跳转的地址范围可参考下面这篇文章:MIPS中分支和跳转的地址范围
2.11 并行与指令:同步
2.12 翻译并启动程序
本节描述了将一个存储在外存(磁盘或闪存)某文件中的C程序转换为计算机上可执行程序的4个步骤。
C语言的翻译层次。用高级语言编写的程序首先被编译为汇编语言程序,然后被汇编为机器语言组成的目标模块。链接器将多个模块和库例程组合在一起解析所有的引用。加载器将机器代码加载到内存的适当位置供处理器执行。为了加速翻译过程,某些步骤被跳过或和其他步骤结合在一起。
图片来自:《计算机组成与设计(ARM版)》英文版
为了识别文件类型,UNIX使用文件的后缀,x.c代表C源文件,x.s表示汇编文件,x.o代表目标文件,x.a代表静态链接库,x.so代表动态链接库,a.out默认情况下表示可执行文件。
MS-DOS使用后缀.C表示C源文件,.ASM代表汇编文件,.OBJ代表目标文件,.LIB代表静态链接库,.DLL代表动态链接库,.EXE代表可执行文件。
2.12.1 编译器
2.12.2 汇编器
2.12.3 链接器
2.12.4 加载器
2.12.5 动态链接库
2.12.6 启动Java程序
Java程序并不会被编译成目标计算机的汇编语言,而是首先被编译成易于解释的指令序列—Java字节码(Java bytecode)指令集。 该指令集被设计得与Java语言接近,因此编译步骤相对简单。事实上,并不需要进行任何优化。就像C语言编译器那样,Java编译器检查数据类型并且为每种数据类型生成正确的操作。Java程序最终将转化成这些字节码的二进制形式。
图片来自:《计算机组成与设计(ARM版)》英文版
一种称为Java虚拟机(Java Virtual Machine,JVM)的软件解释器能够执行Java字节码。解释器是一个用来模拟一种指令集体系结构的程序。例如,这门书提供下载的DS-5,ARMv8模拟器就是一种解释器。由于翻译非常简单,地址可以由编译器填写或在运行时被JVM发现,因此不需要单独的汇编步骤。
解释的优势是可移植性。
解释的不足是性能较差。 和传统方式编译的C程序相比,相差10倍的性能。
为了既能够保持可移植性又提高执行速度,Java发展的下一阶段是实现能够在程序执行同时进行翻译的编译器。这种即时编译器(Just In Time complier,JIT)通过记录运行的程序来找到“热点”(“热点”指程序中运行特别频繁的代码块),然后将它们编译成Java虚拟机所运行于的宿主机上的本地指令。编译过的部分被保存起来供下次程序运行时使用,从而使以后每次运行变得更快。
小测验: 和 翻译器相比,对Java开发者来说,解释器的哪些优点是最重要的?
回答: 机器独立性。即与机器无关。
2.13 综合实例:C排序程序
2.13.1 swap过程
一个将内存中两个不同位置的内容进行交换的C过程。
void swap(long long int v[],size_t k){//数组v,位置k
//交换 v[k]和v[k+1]long long int tmp;tmp=v[k];v[k]=v[k+1];v[k+1]=tmp;
}
我们按照以下步骤把该过程从C语言手动翻译成汇编语言:
- 为程序变量分配寄存器
- 为过程体生成汇编代码
- 保存过程调用间的寄存器
1 为swap分配寄存器
在LEGv8中,使用寄存器X0到X7进行参数传递。由于swap只有两个参数v和k,因此可以被分配给寄存器X0和X1.仅剩的另一个变量是tmp,由于swap是一个叶过程,我们将其分配给寄存器X9.
寄存器分配如下表:
数组v | k | tmp |
---|---|---|
X0 | X1 | X9 |
2 为swap过程体生成代码
LEGv8存储地址按字节编址,因此双字由8个字节组成。因此索引k需先乘上8,再与地址相加。忘记连续的双字之间的地址相差8而不是1,是用汇编语言编程时常见的错误。
因此,第一步通过左移3位来将k乘以8以获得v[k]的的地址:
LSL X10,X1,#3 // X10=K*8
ADD X10,X10,X0 // X10=V+K*8
接下来根据X10取出v[k]的值,并将X10加8得到v[k+1]:(使用寄存器X9和X11)
LDUR X9,[X10,#0] //X9=V[K]
LDUR X11,[X10,#8] //X10=V[K+1]
最后将X9和X11存储到需要交换数据的地址中
STUR X11,[X10,#0]// 交换
STUR X9,[X10,#8]
至此,我们已经为该过程分配了寄存器并获得了实现交换操作的代码。保存swap中使用的保存寄存器的代码并没有包括在其中。但由于我们并不使用叶过程(这里swap是一个叶过程)中的保留寄存器,因此没有需要保留的东西。
3 完整的swap过程
现在得到完整的例程,包括过程标签以及返回的跳转指令。
swap:LSL X10,X1,#3 //k*8ADD X10,X10,X0//得到v[k]LDUR X9,[X10,#0] //取出到寄存器LDUR X11,[X10,#8]STUR X9,[X10,#8]//交换,存储到存储器STUR X11,[X10,#0]BR LR //过程返回
2.13.2 sort过程
该例中,我们将编写一个调用swap过程的例程,使用冒泡排序算法(这种排序算法虽然不是最快的,但确是最简单的)对数组中的整数进行排序。
一个对数组v中元素进行排序的C过程(冒泡排序):
void sort(long long int v[],size_t int n){//数组v和数组长度nsize_t i ,j;for(i=0;i<n;i++){for(j=i-1;j>=0&&v[j]>v[j+1];j--)//从小到大排序swap(v,j);//交换v[j]和v[j+1]}}
1 为sort分配寄存器
寄存器X0和X1分配给过程sort的两个参数v和n,寄存器X19和X20分配给变量i和j。
寄存器分配如下表:
数组v | n | i | j |
---|---|---|---|
X0 | X1 | X19 | X20 |
2 为sort过程体生成代码
过程体包含两个嵌套的for循环和一个带参数的swap过程。下面由外向内来展开代码。
第一步翻译第一个for循环
for(i=0;i<n;i++){
C语言中的for语句有三个部分:初始化、循环条件判断和迭代递增。将i初始化为0只需要一条指令,故for循环的第一部分为:
MOV X19,XZR //i=0
(注意:MOV是为了方便汇编程序员编程而由汇编器提供的伪指令)。将i递增同样也只需要一条指令实现,因此for语句的最后部分为:
ADDI X19,X19,#1 //i+=1
当i<n非真时需要退出循环,换句话说,也就是i≥n时退出循环。循环条件判断需要两条指令:
for1tst: CMP X19,X1 //比较X19和X1(也就是i和n)B.GE exit1 //如果(i≥n)则跳转 到exit1
循环最后仅仅跳回循环条件判断的地方:
B for1tst // branch to test of outer loop
exit1:
第一个for循环的框架代码如下:
MOV X19,XZR
for1tst:CMP X19,X1 //循环条件判断B.GE exit1...(body of first for loop)...ADDI X19,X19,#1//i+=1B for1tst //回到for1tst
exit1:
第二个for循环的C语句如下:
for(j=i-1;j>=0&&v[j]>v[j+1];j--)//从小到大排序
这个循环的初始化部分仍然只需要一条指令:
SUBI X20,X10,#1 //j=i-1
循环末尾j的递减也只需一条指令:
SUBI X20,X20,#1//J-=1;
循环条件测试由两部分组成,任何一个条件为假就退出循环。因此,如果第一个条件测试为假(j<0),循环就要退出:
for2tst:CMP X20,XZRB.LT exit2 //if (j<0) go to exit2
这条跳转指令将跳过第二个条件测试。如果没有跳过,则j≥0.
下面来看第二个条件,当v[j]≤v[j+1]时为假。
获取v[j]
LSL X10,X20,#3
ADD X11,X0,X10// X11存放v[j]
现在取出v[j] (放在寄存器X12中):
LDUR X12,[X11,#0]
因为第二个元素恰好是顺序下一个双字,因此将寄存器X11中的地址值加8就可以取出v[j+1] (存放在X13中):
LDUR X13,[X11,#8]
测试v[j]≤v[j+1],以判断是否跳出循环:
CMP X12,X13
B.LE exit2 //小于等于则跳转
循环末尾跳转到内存循环测试处:
B for2tst
将这些代码片组合起来就可以得到第二个for循环的框架:
SUBI X20,X19,#1 //J=I-1
for2tst:CMP X20,XZR // J AND 0 进行比较B.LT exit2LSL X10,X20,#3ADD X11,X0,X10// X11存放v[j]LDUR X12,[X11,#0] LDUR X13,[X11,#8]CMP X12,X13B.LE exit2 //小于等于则跳转...(body of second for loop)...SUBI X20,X20,#1//J-=1;B for2tstexit2:
3 sort 中的过程调用
下一步处理第二个for循坏体:
swap(v,j);
调用swap足够简单(一条BL指令即可实现):
BL swap
4 sort中的参数传递
当我们想传递参数时问题出现了,因为sort过程需要使用寄存器X0和X1中的值,而swap过程需要将其参数放入相同的寄存器中。一种解决办法是在过程执行的早期就将sort的参数复制到其他寄存器中,让出X0和X1寄存器供swap过程使用。(这种复制要比使用栈进行保存和恢复快得多。)在过程中,首先将寄存器X0和X1的值用如下方法复制到X21和X22中:
MOV X21,X0 // COPY PARAMETER X0 INTO X21
MOVE X22,X1 //COPY PARAMETER X1 INTO X22
然后用下面两条指令将参数传递给swap:
MOV X0,X21 // swap的第一个参数v
MOV X1,X20//swap的第二个参数j
swap(v,j);
5 在sort中保存寄存器
剩下的代码保存和恢复寄存器的值。显然,我们必须将返回地址保存在寄存器LR中,因为sort是一个过程并且本身也被调用。sort过程还使用了由被调用者保存的寄存器X19,X20,X21和X22,这些寄存器的值也必须被保存。因此,sort过程开头的代码如下:
SUBI SP,SP,#40 // MAKE ROOM ON STACK FOR 5 REGS
STUR LR,[SP,#32] //SAVE LR ON STACK
STUR X22,[SP,#24] // SAVE X22
STUR X21,[SP,#16] //SAVE X21
STUR X20,[SP,#8] //SAVE X20
STUR X19,[SP,#0] //SAVE X19
过程末尾只需要简单地反向执行这些指令,最后加入一条BR指令以实现返回。
6 完整的sort过程
注意for循环中对寄存器X0和X1的引用被替换成对寄存器X21和X22的引用。
//保存寄存器的值
sort: SUBI SP,SP,#40 // MAKE ROOM ON STACK FOR 5 REGSSTUR LR,[SP,#32] //SAVE LR ON STACKSTUR X22,[SP,#24] // SAVE X22STUR X21,[SP,#16] //SAVE X21STUR X20,[SP,#8] //SAVE X20STUR X19,[SP,#0] //SAVE X19
//过程体//传送参数MOV X21,X0 // COPY PARAMETER X0 INTO X21MOVE X22,X1 //COPY PARAMETER X1 INTO X22//外部循环SUBI X20,X19,#1 //J=I-1
for2tst:CMP X20,XZR // J AND 0 进行比较B.LT exit2LSL X10,X20,#3ADD X11,X0,X10// X11存放v[j]LDUR X12,[X11,#0] LDUR X13,[X11,#8]CMP X12,X13B.LE exit2 //小于等于则跳转//传递参数和调用MOV X0,X21 // swap的第一个参数vMOV X1,X20//swap的第二个参数jBL swap//内部循环SUBI X20,X20,#1//J-=1;B for2tst//外部循环
exit2: ADDI X19,X19,#1B for1tst//恢复寄存器的值
exit1: STUR X19,[SP,#0] //SAVE X19STUR X20,[SP,#8] //SAVE X20STUR X21,[SP,#16] //SAVE X21STUR X22,[SP,#24] // SAVE X22STUR LR,[SP,#32] //SAVE LR ON STACKSUBI SP,SP,#40 // MAKE ROOM ON STACK FOR 5 REGS//过程返回BR LR //RETURN TO CALLING ROUTINE
《计算机组成与设计(ARM版)》读书笔记-第二章指令2相关推荐
- 《Spring实战》第四版读书笔记 第二章 装配Bean
2019独角兽企业重金招聘Python工程师标准>>> 在Spring中,对象无需自己查找或创建与其所关联的其他对象.相反,容器负责把需要相互协作的对象引用赋予各个对象. 创建应用对 ...
- 《计算传播学导论》读书笔记——第二章文本分析简介
<计算传播学导论>读书笔记--第二章文本分析简介 第一节 文本分析研究现状 常用文本挖掘技术 第二节 文本分析与传播学研究 (一)为什么文本挖掘技术逐渐受到传播学者的关注 (二)不同文本分 ...
- 计算机网络(第五版 作者:AndrewS.Tanenbaum David J.Wetherall 清华大学出版社)读书笔记----第二章的学习
计算机网络第二章--物理层读书笔记 1.物理层是网络的技术设置,物理层的材质和带宽决定了最大的传输速率. 2.传输介质的分类:引导性(有线介质)和非引导性(无线介质). (1)有线介质:磁介质.双绞线 ...
- PHP核心技术与最佳实践 读书笔记 第二章 面向对象的设计原则
2019独角兽企业重金招聘Python工程师标准>>> 第二章 面向对象的设计原则 2.1 面向对象设计的五大原则 单一职责原则 接口隔离原则 开放-封闭原则 替换原则 依赖倒置原则 ...
- C++Primer读书笔记----第二章变量和基本类型
前段时间学习iphone开发,非常力不从心,很多C/C++的类库代码看不明白.大学的时候都学过C/C++,但是基本上忘光了,现在做C#开发,经常写出来一些性能低下的代码,究其原因就是原理掌握不扎实,乱 ...
- C++ Primer Plus读书笔记第二章
自学了一段时间的C++打算还是要系统的整理一下一些知识点,让学习思路更清晰,不然老是学一点忘一点,这个读书笔记用来记录这段时间对C++ Primer Plus一书中知识点的记录,尽量会写的详细一点.直 ...
- 《软件测试经验与教训》读书笔记---第二章
<软件测试经验与教训>读书笔记--目录 第一章 测试员的角色 第二章 按测试员的方式思考 第三章 测试手段 第四章 程序错误分析 第五章 测试自动化 第六章 测试文档 第七章 与程序员交互 ...
- In-memory Computing with SAP HANA读书笔记 - 第二章:SAP HANA overview
本文为In-memory Computing with SAP HANA on Lenovo X6 Systems第二章SAP HANA overview的读书笔记. 本章最重要的部分是SAP HAN ...
- 《Process Analytics 过程分析》读书笔记 第二章 业务过程范例
第二章 业务过程范例 毫无疑问,在过程管理中的研究和开发为技术革命铺平了道路. 更重要的是,过程管理技术在今天也同样重要,就像它在两三年前成立时一样. 然而,主要转移的是主要重点. 例如,在1990年 ...
最新文章
- SAP WM 针对采购订单收货时候不能自动获取物料主数据里的Special Movement Indicator?
- 图像“颜色选择”怎么用?
- 疫情退票引爆的潘多拉盒子,境外旅游商家濒临倒闭
- php long2ip,php 中IPV6 ip2long的问题解决办法
- servlet 配置 使用_配置HTTPS以与Servlet一起使用
- useReducer使用和原理
- 用树莓派3B+实现智能语音识别
- 在线二进制转文本字符工具
- 蓝宝石vega56刷64bios及降压超频全过程
- XMAN选拔赛官方Writeup
- android 夜间模式功能,Android 夜间模式的三种实现
- mysql常用日期的写法
- 深入React v16新特性(二)
- 独立版微信动态二维码活码管理系统免授权版
- JS判断输入的字符串是否是数字(正则表达式)
- 多易教育KAFKA实战(2)-java生产者客户端API示例代码
- 勤于奋国外LEAD联盟赚钱注意事项
- 卷积神经网络(CNN)相关知识以及数学推导
- java类编来那个初始化顺序_java类的初始化顺序
- 供应链信息系统建设思路
热门文章
- mysql某个表被行锁了_MySQL中的锁(表锁、行锁)
- 手机bootstrap搜索框_你知道手机可以对摄像头进行图像调节吗
- 用神经网络分类响尾蛇和牛蛙
- 比较双曲正切tanh与sigmoid激活函数的性能差异
- jdbc配置文件连接mysql_java jdbc使用配置文件连接数据库:
- 【UAV】从单个螺旋桨到四旋翼无人机运动学分析
- 【控制】《鲁棒控制-线性矩阵不等式处理方法》-俞立老师-第7章-保性能控制
- Python 实现图片质量比较之PSNR和SSIM
- STM32 电机教程 16 - PMSM电机磁场定向控制原理
- Scan Chain的原理与实现(实践) - Compression Flow