csapp 第五章读书笔记
Optimizing Program Performance
- 数据结构和算法
- compiler 优化: eliminating unnecessary function calls, conditional tests, and memory references, instruction-level parallelism,
- parallelism
Capabilities and Limitations of Optimizing Compilers
twiddle2 会更高效,因为twiddle2 只需要3次memory references (read *xp, read *yp, write *xp), 但是 twiddle1 需要6次 (two reads of *xp, two reads of *yp, 和 two writes of *xp).
line3,*xp +=2, *xp=4, line4, *xp+=4, *xp=8
line9, *xp += 2 *xp, *xp = 6
所以编译器不能把twiddle2作为twiddle1的优化版本。
1st optimization blocker:
The value computed for t1 depends on whether or not pointers p and q are aliased---if not, it will equal 3,000, but if so it will equal 1,000.
memory aliasing 是指两个或多个不同的变量或指针引用相同的内存位置。这种情况可能会导致意外的行为,例如在修改一个变量的值时意外地影响另一个变量的值。内存别名通常是由于指针操作或引用传递而引起的,程序员需要注意并避免潜在的内存别名问题。
2nd optimization blocker:
func1 调用f()4次,func2 调用f()1次。
This function has a side effect---it modifies some part of the global program state.
所以func1 修改4次,func2修改1次。
call to func1 would return 0 + 1+ 2 + 3= 6, whereas a call to func2 would return 4 . 0 = 0, assuming both started with global variable counter set to zero.
Expressing Program Performance
引入了每个元素的循环指标,简称为CPE。
循环指标(CPE)是衡量计算机程序中每个元素所需的处理周期数的度量单位。它是通过将程序的总运行周期数除以处理的元素数量来计算得出的。CPE越低,表示程序在执行相同任务时效率越高。因此,较低的CPE值通常意味着更高的性能和更好的优化。(a/b通常表示a除以b)
The sequencing of activities by a processor is controlled by a clock providing a regular signal of some frequency, usually expressed in gigahertz (GHz), billions of cycles per second. 处理器对活动的排序是由时钟控制的,时钟提供一种频率的规律信号,通常以千兆赫(GHz)表示,即每秒数十亿个周期。
举个例子,a "4 GHz" processor, it means that the processor clock runs at 4.0 × 10 ^9 cycles per second. 如果一个处理器是"4 GHz"的,意味着处理器时钟以每秒4.0 × 10 ^9个周期运行。These typically are expressed in nanoseconds (1 nanosecond is 10 ^−9 seconds) or picoseconds (1 picosecond is 10 ^−12 seconds). the period of a 4 GHz clock can be expressed as either 0.25 nanoseconds or 250 picoseconds. 0.25 nanoseconds == 1/ (4.0 × 10 ^9)
函数psum1每次迭代计算结果向量的一个元素。函数psum2每次迭代计算结果向量的两个元素。
简单介绍下,循环展开(loop unrolling)是一种优化技术,用于提高计算机程序的性能。在循环展开中,程序员或编译器会将循环体内的代码重复多次,以减少循环迭代的开销。这样可以减少循环控制的开销,并且有助于提高指令级并行性,因为更多的代码可以同时执行。循环展开可以提高程序的性能,但也会增加代码的大小,因此需要权衡考虑。之后会展开讲解loop unrolling。
psum2在相同元素数量的情况下,需要的cycle更少。
Program Example
cpp
typedef long data_t;
data_t 可以被定义成int, long, float 或者 double, 方便对比cpe。
By using different definitions of compile-time constants IDENT and OP, the code can be recompiled to perform different operations on the data. 通过使用不同的编译常量IDENT和OP,可以重新编译代码以对数据执行不同的操作。
对比了combine1 使用int 和 float的区别,以及使用了不同等级的编译优化。
对许多不同程序进行的实验表明,对于32位和64位整数的操作在性能上是相同的,除了涉及除法操作的代码。
The unoptimized code provides a direct translation of theCcode into machine code, often with obvious inefficiencies.
Eliminating Loop Inefficiencies
combine1 函数值调用多次vec_length函数, 而combine2 函数调用1次vec_length函数。
代码移动(code motion)是一种优化技术,它通过将计算操作从循环内部移动到循环外部来提高程序的性能。这种优化技术的目标是减少重复计算和内存访问,从而提高程序的执行速度。
在循环中,如果某个计算操作的结果在每次迭代中都保持不变,那么将该操作移动到循环外部可以避免重复计算,并减少循环内部的计算量。这样可以减少程序的总执行时间,尤其是对于复杂的计算操作或者需要频繁访问内存的情况。
代码移动的关键是确定哪些计算操作可以安全地移动到循环外部。这需要考虑数据的依赖关系和循环的执行顺序。如果一个计算操作的结果在循环内部被使用,并且该结果在每次迭代中都会改变,那么将其移动到循环外部可能会导致错误的结果。
代码移动是一种常见的优化技术,广泛应用于编译器和程序优化工具中。它可以显著提高程序的性能,并减少执行时间和资源消耗。但是,代码移动也可能引入额外的开销,例如增加内存访问次数或者增加代码的复杂性。因此,在进行代码移动优化时需要进行综合考虑,并进行合适的权衡。
在lower1的n次迭代中都调用了strlen,strlen 是o(n), 所以lower1是o(n^2)。
Reducing Procedure Calls
combine2 和 combine3 的区别是get_vec_start函数从函数循环体中移出。
没有明显的性能改进。
Eliminating Unneeded Memory References
vmovsd (%rbx), %xmm0的含义是将内存地址存储在%rbx寄存器中的双精度浮点数加载到%xmm0寄存器中。
vmulsd (%rdx), %xmm0, %xmm0 的含义是将内存地址存储在%rdx寄存器中的双精度浮点数与%xmm0寄存器中的双精度浮点数相乘,并将结果存储回%xmm0寄存器中。
the address corresponding to pointer dest is held in register %rbx. a pointer to the ith data element in register %rdx, shown in the annotations as data+i.
The loop termination is detected by comparing this pointer to one stored in register %rax.
combine3的函数循环里,每次迭代都会更新*dest的值,所以会造成读写的浪费。
combine3 和 combine4的区别是 dest移出函数循环体。使用local variable代替 dest,消除了对内存的读写。
create an alias between the last element of the vector and the destination for storing the result.
combine3函数,line8, 修改v[2] =1, 然后,combine3 的v[2] 会一直变化,这个是memory aliasing。
combine4函数,使用的是acc, 只有loop 执行结束后,会更新v[2]。
combine4 提高了很多。
Understanding Modern Processors
instruction-level parallelism
The latency bound is encountered when a series of operations must be performed in strict sequence, because the result of one operation is required before the next one can begin. This bound can limit program performance when the data dependencies in the code limit the ability of the processor to exploit instruction-level parallelism.
The throughput bound characterizes the raw computing capacity of the processor's functional units. This bound becomes the ultimate limit on program performance.
Overall Operation
superscalar,超标量处理器是一种能够同时执行多条指令的处理器。它通过在一个时钟周期内执行多个指令,从而提高了处理器的性能。超标量处理器通常包括多个执行单元,可以同时执行多个指令,以实现并行处理。这种设计可以显著提高处理器的吞吐量,从而加快程序的执行速度。
instruction control unit,指令控制单元(ICU)负责从内存中读取一系列指令,并从中生成一组原始操作,以在程序数据上执行。
execution unit,执行单元(EU)负责执行这些操作。
branch prediction:
投机执行(Speculative Execution)是一种计算机处理器优化技术,它允许处理器在缺乏足够信息的情况下预测分支指令的结果,并在预测的分支方向上继续执行指令。这样可以提高处理器的性能,因为它可以在等待分支指令结果的同时继续执行其他指令。然而,如果预测错误,处理器必须废弃已经执行的操作,这可能会导致性能损失。投机执行在现代处理器中得到广泛应用,但也因为与安全漏洞(如Spectre和Meltdown)有关而引起了关注。
In a typical x86 implementation, an instruction that only operates on registers, such as addq %rax,%rdx is converted into a single operation.
an instruction involving one or more memory references, such as addq %rax,8(%rdx) yields multiple operations, separating the memory references from the arithmetic operations.
关于register renaming, 1st instruction 更新register, entry(r,t) 会被添加到renaming table, 当exe unit完成了1st instruction, 会产生一个result(v,t),其他的等待t的操作,会使用v,一种forwarding。renaming table 只记录有写操作的指令有关的entry。
Functional Unit Performance
latency,延迟是指执行该操作所需的总时间;
issue,是指相同类型的两个独立操作之间的最小时钟周期数;
capacity,表示能够执行该操作的功能单元的数量。
An Abstract Model of Processor Operation
The CPE is therefore equal to the latency bound L.
From Machine-Level Code to Data-Flow Graphs
registers %rdx holding a pointer to the ith element of array data,
%rax holding a pointer to the end of the array,
and %xmm0 holding the accumulated value acc.
the four instructions are expanded by the instruction decoder into a series of five operations
4类寄存器:
the comparison (cmp) and branch (jne) operations do not directly affect the flow of data in the program,所以是白色的。
The purpose of the compare and branch operations is to test the branch condition and notify the ICU
5.14 b可以看出来数据间的依赖关系,后一个%xmm0 会依赖于 前一个%xmm0。
floating-point multiplication has a latency of 5 cycles, while integer addition has a latency of 1 cycle
所以一个是5n cycles, 一个是n cycles。integer addition 执行结束后,会等待floating-point multiplication 执行结束,进入下一次迭代。
For all of the cases where the operation has a latency L greater than 1, we see that the measured CPE is simply L, indicating that this chain forms the performance-limiting critical path.
对于所有操作的延迟(latency)大于1的情况,我们可以看到测得的CPE(cycles per element)简单地等于延迟L,这表明这个链条形成了性能限制的关键路径。
Other Performance Factors
关键路径仅提供了程序所需的最低循环数。其他因素也可以限制性能,包括可用的功能单元总数以及在任何给定步骤中可以在功能单元之间传递的数据值的数量。
Loop Unrolling
对比combine4, acc = acc op data[i], combine5, 是acc =acc op data[i] op data[i+1], 一次迭代,操作2个元素。如果i不是二的倍数,所以有line 15-19。
unroll a loop 也可以任意k个元素,例如 acc = acc op data[i] ... op data[i+k-1], 第一个loop 是i< n-k+1, 第二个loop的范围是0到k-1。
k==2 或 3 时,有较为明显的性能提升。
前两条指令都是执行双精度浮点数乘法操作。第一条指令从内存中读取数据时使用的是基址和偏移量的和作为地址,而第二条指令在基址和偏移量的和上再加上8个字节的位置作为地址。
Loop index i is held in register %rdx, and the address of data is held in register %rax.
the accumulated value acc is held in vector register %xmm0.
图中仍然存在一个关键路径,其中有n个乘法操作。虽然迭代次数减少了一半,但每次迭代仍然有两个连续的乘法操作。由于关键路径是没有loop unrolling的代码性能的限制因素,因此在进行k×1 loop unrolling后,关键路径仍然是限制性能的因素。
循环展开可以很容易地由编译器执行。许多编译器将此作为其优化集合的一部分进行操作。当使用优化级别3或更高级别调用gcc时,它将执行某些形式的循环展开。
Enhancing Parallelism
执行加法和乘法的功能单元都是完全流水线化的,这意味着它们可以在每个时钟周期开始新的操作,并且一些操作可以由多个功能单元执行。
硬件有潜力以更高的速率执行乘法和加法,但是我们的代码无法利用这种能力,即使进行了loop unrolling,也是将值累积为单个变量acc。
在前一个计算完成之前,我们无法计算出acc的新值。即使计算新的acc值的功能单元可以在每个时钟周期开始新的操作,但它每L个周期才会开始一个操作,其中L是组合操作的延迟。现在,将研究如何打破这种顺序依赖关系,并获得超过延迟限制的性能。
Multiple Accumulators
处理器不再需要延迟开始一个求和或乘积操作,直到前一个操作完成。
Each of these critical paths contains only n/2 operations, thus leading to a CPE of around 5.00/2 = 2.50.
the multiple accumulator transformation to unroll the loop by a factor of k and accumulate k values in parallel, yielding k × k loop unrolling
For an operation with latency L and capacity C, this requires an unrolling factor k ≥ C . L.
For example, floating-point multiplication has C = 2 and L = 5, necessitating an unrolling factor of k ≥ 10.
Floating-point addition has C = 1 and L = 3, achieving maximum throughput with k ≥ 3.
通常情况下,只有当程序能够保持所有能够执行该操作的功能单元的流水线填充时,才能达到操作的吞吐量上限。对于具有延迟L和容量C的操作,这需要一个展开因子k≥C.L。例如,浮点乘法具有C = 2和L = 5,需要展开因子k≥10。浮点加法具有C = 1和L = 3,在k≥3时实现最大吞吐量。
integer data type:
an optimizing compiler could potentially convert the code shown in combine4 first to a two-way unrolled variant of combine5 by loop unrolling, and then to that of combine6 by introducing parallelism.
floating-point data type:
floating-point multiplication and addition are not associative.
Thus, combine5 and combine6 could produce different results due to rounding or overflow.
Reassociation Transformation
第一个load 从内存加载 data[i] ,第二个load和和第一个mul 把内存加载第data[i+1]乘以data[i],第二个乘法,acc和(data[i] * data[i+1])的结果。
The first multiplication within each iteration can be performed without waiting for the accumulated value from the previous iteration.Thus, we reduce the minimum possible CPE by a factor of around 2.
a reassociation transformation can reduce the number of operations along the critical path in a computation, resulting in better performance by better utilizing the multiple functional units and their pipelining capabilities.
重新关联转换可以减少计算中关键路径上的操作数量,通过更好地利用多个功能单元及其流水线能力,从而实现更好的性能。
Most compilers will not attempt any reassociations of floating-point operations, since these operations are not guaranteed to be associative. 大多数编译器不会尝试重新关联浮点操作,因为这些操作不保证是可结合的。
Summary of Results for Optimizing Combining Code
以下总结了我们使用标量代码获得的结果,未利用AVX vector instructions:
Some Limiting Factors
the critical path in a data-flow graph representation of a program indicates a fundamental lower bound on the time required to execute a
program. 程序的数据流图表示中的关键路径指示了执行程序所需的基本时间下限。
if there is some chain of data dependencies in a program where the sum of all of the latencies along that chain equals T , then the program will require at least T cycles to execute. 如果程序中存在一些数据依赖的链条,其中沿着该链条的所有延迟之和等于T,则程序至少需要T个周期来执行。
除此以外,功能单元的吞吐量边界也对程序的执行时间施加了下限。也就是说,假设程序需要执行某种操作的总计算量为N,微处理器具有能够执行该操作的C个功能单元,并且这些单元的发射时间为I。那么程序至少需要N . I/C个周期来执行。
还需要考虑一些其他限制实际机器上程序性能的因素。
Register Spilling
Once the number of loop variables exceeds the number of available registers, the program must allocate some on the stack.
Once a compiler must resort to register spilling, any advantage of maintaining multiple accumulators will most likely be lost.
The program must read both its value and the value of data[i] from memory, multiply them, and store the result back to memory.
Once a compiler must resort to register spilling, any advantage of maintaining multiple accumulators will most likely be lost.
Branch Prediction and Misprediction Penalties
The basic idea for translating into conditional moves is to compute the values along both branches of a conditional expression or statement and then use conditional moves to select the desired value.
以下是一个示例,展示了如何使用条件移动指令来选择两个值中的一个:
假设有两个整数变量a和b,我们想要选择其中较大的值并将其存储在变量c中。
使用条件移动指令,可以按照以下步骤执行:
- 计算a和b的差值:diff = a - b
- 将diff与0进行比较,如果diff大于0,则表示a大于b,否则表示a小于等于b。
- 使用条件移动指令选择较大的值:c = a - (diff & 0xFFFFFFFF)
这个例子中,我们首先计算a和b的差值,然后将差值与0进行比较。如果差值大于0,说明a大于b,我们选择a作为较大的值;否则,我们选择b作为较大的值。最后,我们使用条件移动指令将较大的值存储在变量c中。
请注意,上述示例是基于32位整数的情况。对于其他数据类型和操作,可能需要使用不同的条件移动指令或逻辑运算来实现相应的选择操作。
C程序员如何确保分支预测错误不影响程序的效率?对于这个问题,没有简单的答案,但以下一般原则适用。
-
Minimize Branches: 尽量减少分支语句的使用。分支语句会增加分支预测错误的可能性。可以通过重构代码或使用其他算法来避免或减少分支语句的数量。
-
Use Predictable Branches: 使用可预测的分支语句。某些分支模式更容易预测,例如循环中的递增或递减分支。尽量使用这些可预测的分支模式,以提高分支预测的准确性。
-
Profile and Optimize: 使用性能分析工具来确定哪些分支语句导致了较高的分支预测错误率。然后,针对这些分支语句进行优化,例如重构代码或使用其他控制结构来避免分支。
-
Use Compiler Optimizations: 使用编译器的优化选项。现代编译器通常具有优化功能,可以自动转换代码以减少分支预测错误的可能性。启用编译器优化选项可以提高程序的效率。
-
Consider Branchless Programming: 考虑无分支编程。有时,可以通过重写代码,使用条件移动或位操作等技术来避免分支语句,从而减少分支预测错误。
这些是一些通用的原则,可以帮助C程序员减少分支预测错误对程序效率的影响。然而,具体的优化方法和技术可能因程序和硬件环境而异,需要根据具体情况进行调整和优化。
Do Not Be Overly Concerned about Predictable Branches
the branch prediction logic found in modern processors is very good at discerning regular patterns and long-term trends for the different branch instructions. 现代处理器中的分支预测逻辑非常擅长识别不同分支指令的规律模式和长期趋势。
通常会将循环结束的分支预测为被执行,因此只会在最后一次循环时产生分支预测错误的惩罚。
再举一个例子,考虑我们从combine2过渡到combine3时观察到的结果,当我们将函数get_vec_element从函数的内部循环中提取出来时,如下所示:
尽管消除了每次迭代中检查向量索引是否在范围内的两个条件判断,但CPE并没有改善。对于这个函数来说,这些检查总是成功的,因此它们是高度可预测的。
Write Code Suitable for Implementation with Conditional Moves
For inherently unpredictable cases, program performance can be greatly enhanced if the compiler is able to generate code using conditional data transfers rather than conditional control transfers.
imperative style of implementing:
Our measurements for this function show a CPE of around 13.5 for random data and 2.5--3.5 for predictable data, an indication of a misprediction penalty of around 20 cycles.
functional style of implementing:
Our measurements for this function show a CPE of around 4.0 regardless of whether the data are arbitrary or predictable. (We also examined the generated assembly code to make sure that it indeed uses conditional moves.
Understanding Memory Performance
Load Performance
The performance of a program containing load operations depends on both the pipelining capability and the latency of the load unit.
CPE of 4.00
The movq instruction on line 3 forms the critical bottleneck in this loop.
The CPE of 4.00 for this function is determined by the latency of the load operation.
this measurement matches the documented access time of 4 cycles for the reference machine's L1 cache.
这个函数的CPE(Cycles Per Element)为4.00是由加载操作的延迟决定的。这个测量结果与参考机器的L1缓存的文档访问时间4个周期相匹配。
Store Performance
a series of store operations cannot create a data dependency.
Only a load operation is affected by the result of a store operation, since only a load can read back the memory value that has been written by the store.
example A 的 CPE 是1.3。
example B的 CPE 是7.3。arguments src and dest pointing to the same memory location. The write/read dependency causes a slowdown in the processing of around 6 clock cycles.
The store unit includes a store buffer containing the addresses and data of the store operations that have been issued to the store unit, but have not yet been completed, where completion involves updating the data cache.
存储缓冲区包含已经发出到存储单元但尚未完成的存储操作的地址和数据,完成涉及更新数据缓存。
This buffer is provided so that a series of store operations can be executed without having to wait for each one to update the cache.
If it finds a match (meaning that any of the bytes being written have the same address as any of the bytes being read), it retrieves the corresponding data entry as the result of the load operation.
t 就是val
The s_addr instruction computes the address for the store operation, creates an entry in the store buffer, and sets the address field for that entry. The s_data operation sets the data field for the entry. s_addr和s_data 的s是store。
the address computation of the s_addr operation must clearly precede the s_data operation.
the load operation generated by decoding the instruction movq (%rdi), %rax must check the addresses of any pending store operations, creating a data dependency between it and the s_addr operation
The figure shows a dashed arc between the s_data and load operations. This dependency is conditional: if the two addresses match, the load operation must wait until the s_data has deposited its result into the store buffer, but if the two addresses differ, the two operations
can proceed independently. 地址相同,需要等待;地址不同,相互独立;
The arc labeled "1" represents the requirement that the store address must be computed before the data can be stored. s_addr在s_data 之前。需要buffer里有s_addr, 才能往buffer里写数据。
The arc labeled "2" represents the need for the load operation to compare its address with that for any pending store operations. load指令会比较load的地址和没有完成的store指令的地址。
The dashed arc labeled "3" represents the conditional data dependency that arises when the load and store addresses match. 条件数据依赖关系存在于s_data和load。
Aside
Optimizing function calls by inline substitution
"内联替换"是指编译器在编译过程中将函数调用处的函数体直接替换进去,而不是实际上进行函数调用。这样可以减少函数调用的开销,因为不需要保存现场和恢复现场,也不需要跳转到函数的地址执行。内联替换通常会增加代码的大小,因为函数体会被复制到每个调用点,所以通常只对简短的函数进行内联替换。内联替换可以通过关键字inline来指示编译器进行,但是编译器可以选择忽略这个指示。
What is a least squares fit?
最小二乘拟合(Least squares fit)是一种用于拟合数据的统计方法。它的目标是找到一个函数模型,使得该模型预测的值与实际观测值之间的残差平方和最小。这个方法常用于回归分析,通过最小化误差的平方和来找到最佳拟合直线或曲线。最小二乘拟合可以用于估计线性模型的参数,也可以扩展到非线性模型。
可以通过对E(m, b)分别关于m和b进行求导并将它们设置为0来推导出计算m和b的算法。
typedef
在C++中,typedef是一种用于为现有的数据类型创建新的名称的关键字。它允许程序员为复杂的数据类型定义简单的别名,以提高代码的可读性和可维护性。通过typedef,可以为任何现有的数据类型(如int、char、结构体、指针等)创建一个新的名称,然后可以使用这个新的名称来声明变量。typedef的语法如下:
cpp
typedef <existing_type> <new_name>;
例如,可以使用typedef为现有的数据类型创建一个新的名称,如下所示:
cpp
typedef int Integer; // 为int创建一个新的名称Integer
typedef char* StringPtr; // 为char*创建一个新的名称StringPtr
然后可以使用这些新的名称来声明变量,如下所示:
cpp
Integer num = 10; // 声明一个整型变量
StringPtr str = "Hello"; // 声明一个指向字符的指针变量
通过使用typedef,可以使代码更易读,更易维护,并且可以提高代码的可移植性。
define
#define是C/C++中的预处理指令之一,用于定义宏。它允许我们在代码中使用符号常量来代替特定的值或表达式。通过使用#define,我们可以提高代码的可读性和可维护性,并且可以方便地在整个代码中进行修改。
定义一个宏使用的语法如下:
#define 宏名称 替换文本
例如,我们可以使用#define定义一个常量:
#define PI 3.14159
在代码中使用宏时,编译器会将宏名称替换为相应的替换文本。例如,如果我们在代码中使用PI,编译器会将其替换为3.14159。
宏定义可以在任何地方进行,通常放在代码文件的顶部或头文件中。需要注意的是,宏定义没有分号结尾。
inline
内联是一种编译器优化技术,用于减少函数调用的开销。当一个函数被声明为内联函数时,编译器会尝试在调用该函数的地方直接插入函数体,而不是通过函数调用的方式执行。这样可以避免函数调用的开销,例如保存和恢复寄存器、参数传递和返回值处理等。
通过内联函数,可以在不增加额外开销的情况下,将函数的代码嵌入到调用它的地方。这对于一些短小且频繁调用的函数特别有用,可以提高程序的执行效率。但是,内联函数的代码会被复制到每个调用它的地方,可能会增加代码的体积,因此需要权衡代码大小和性能。
在使用内联函数时,程序员可以使用关键字"inline"来声明一个函数为内联函数。然而,编译器并不一定会完全遵循这个声明,它可能会根据自己的优化策略来决定是否将函数内联。因此,程序员应该将内联函数的声明和定义放在同一个头文件中,以便编译器能够在需要的地方进行内联优化。
vmovsd 指令
vmovsd是x86架构指令集中的一条指令,用于在浮点寄存器和内存之间传输双精度浮点数。
该指令的语法如下:
vmovsd destination, source
其中,destination表示目标操作数,可以是一个寄存器或内存地址;source表示源操作数,也可以是一个寄存器或内存地址。
vmovsd指令可以用于将双精度浮点数从内存加载到寄存器,或将寄存器中的双精度浮点数存储到内存中。它还可以在寄存器之间传输双精度浮点数的值。
该指令在处理双精度浮点数的计算和数据传输时非常有用,可以提高程序的性能和效率。
vmulsd 指令
vmulsd是x86架构指令集中的一条指令,用于执行两个双精度浮点数的乘法操作。
该指令的语法如下:
vmulsd destination, source
其中,destination表示目标操作数,可以是一个寄存器或内存地址;source表示源操作数,也可以是一个寄存器或内存地址。
vmulsd指令将目标操作数和源操作数中的双精度浮点数进行乘法运算,并将结果存储在目标操作数中。如果目标操作数和源操作数都是寄存器,则直接在寄存器中执行乘法运算;如果其中一个是内存地址,则从内存中加载数据后执行乘法运算,并将结果存储回内存。
该指令可以用于执行浮点数的乘法运算,例如在科学计算、图形处理和金融应用等领域。它提供了高效的双精度浮点数乘法操作,有助于提高程序的性能和效率。
SSE 和 AVX
SSE (Streaming SIMD Extensions)和AVX (Advanced Vector Extensions)是一组指令集扩展,用于增强处理器的并行计算能力。
SSE是由英特尔推出的一组SIMD指令集扩展,最初是为了提高多媒体和图形应用程序的性能。它引入了一组128位寄存器(XMM寄存器),可以同时处理多个数据元素。SSE指令集包括一系列的算术、逻辑、移位和数据转移指令,可以在单个指令中对多个数据进行并行操作。
AVX是SSE的进一步扩展,引入了256位寄存器(YMM寄存器),可以处理更多的数据元素。AVX指令集增加了更多的指令,包括更复杂的浮点运算、向量操作和数据加载/存储指令,进一步提高了处理器的并行计算能力。
SSE和AVX指令集扩展广泛应用于科学计算、图像处理、视频编码和解码等需要高性能并行计算的领域。它们可以显著提高程序的执行速度,特别是对于那些需要处理大量数据的任务。
介绍 retirement unit
"Retirement unit" 是一个术语,通常在计算机架构和微处理器设计中使用。
在处理器的流水线执行过程中,指令会按照顺序进入流水线的不同阶段进行处理。在执行完最后一个阶段(写回阶段)后,指令就被认为是"退休"了,也就是完成了它的任务并从流水线中移除。
在现代处理器中,有一个专门的硬件单元被称为"退休单元"(retirement unit),用于管理指令的退休过程。它负责确保指令按照正确的顺序退休,以保持程序的正确执行。退休单元会将指令的结果写回到寄存器文件中,更新程序状态,同时还会处理相关的异常和中断。
退休单元的设计和实现对于处理器的性能和正确性都非常重要。它需要能够正确处理各种情况下的指令退休,包括乱序执行、分支预测错误等。同时,退休单元还需要具备高效的处理能力,以确保指令的退休过程不会成为整个流水线的瓶颈。
总之,退休单元是处理器中负责管理指令退休过程的硬件单元,它确保指令按照正确的顺序完成任务,并更新程序状态。它在处理器的性能和正确性方面起着重要的作用。
介绍 register renaming
寄存器重命名(register renaming)是一种在计算机体系结构中使用的技术,用于提高指令级并行性和指令流水线的效率。
在传统的计算机体系结构中,指令流水线中的指令需要依次执行,并且每个指令都需要使用特定的寄存器来存储结果。这种情况下,如果两条指令使用相同的寄存器,那么第一条指令的结果必须等到被第二条指令使用之后才能被写入寄存器。这会导致指令之间的依赖关系,限制了指令的并行执行。
通过寄存器重命名技术,每个指令都被分配了一个唯一的重命名寄存器,而不是直接使用物理寄存器。这样,多个指令可以并行地使用相同的物理寄存器,因为它们实际上使用的是不同的重命名寄存器。这样一来,指令之间的依赖关系就被消除了,指令可以更加自由地并行执行。
寄存器重命名技术的实现需要一个重命名表(renaming table)来维护物理寄存器和重命名寄存器之间的映射关系。当指令需要读取或写入寄存器时,重命名表会将其映射到相应的重命名寄存器或物理寄存器上。这样,指令可以通过重命名寄存器来传递结果,而不需要等待其他指令完成。
通过寄存器重命名技术,可以有效地提高指令级并行性和指令流水线的效率,允许更多的指令并行执行,从而提高程序的性能。
介绍renaming table
重命名表(renaming table)是在寄存器重命名技术中使用的数据结构。它用于跟踪物理寄存器和逻辑寄存器之间的映射关系。
在传统的处理器中,逻辑寄存器直接映射到物理寄存器。但在寄存器重命名技术中,每个逻辑寄存器可以被映射到多个物理寄存器中的一个。这样做的目的是允许指令的执行可以并行进行,提高指令级并行性。
重命名表是一个表格,其中的每一项对应一个逻辑寄存器。每个表项包含两个字段:逻辑寄存器标识符和物理寄存器标识符。逻辑寄存器标识符用于标识逻辑寄存器,物理寄存器标识符用于标识分配给逻辑寄存器的物理寄存器。
当一个指令需要使用一个逻辑寄存器时,首先会在重命名表中查找对应的物理寄存器标识符。然后,指令会使用这个物理寄存器来执行操作。这样,即使多个指令使用相同的逻辑寄存器,它们也可以并行地使用不同的物理寄存器,避免了数据冒险和依赖。
重命名表在指令执行过程中会不断更新,以反映逻辑寄存器与物理寄存器之间的映射关系的变化。这样,指令可以在保持正确性的同时,充分利用可用的物理寄存器,提高处理器的性能和效率。
介绍 Loop unrolling
Loop unrolling是一种优化技术,用于改善循环的执行效率。它通过减少循环迭代的次数,从而减少循环控制开销和分支开销,并增加指令级并行性。
在循环中,重复执行相同的操作可以导致性能瓶颈。Loop unrolling通过将多个迭代的操作合并为一个更大的循环体,从而减少循环的迭代次数。这样一来,每个循环迭代中的操作数目增加,从而减少了循环控制开销和分支开销。此外,由于循环体变大,可以更好地利用指令级并行性,即在同一时间内执行多个操作。
例如,考虑以下的简单循环:
for (int i = 0; i < 6; i++) {
// 操作
}
通过将循环展开成两次迭代:
// 第一次迭代
// 操作
// 第二次迭代
// 操作
这样,循环的迭代次数减少了一半,减少了循环控制和分支开销。同时,每次迭代中的操作数目增加,提高了指令级并行性。
然而,Loop unrolling也有一些限制。首先,展开循环会增加代码的长度,可能会导致缓存不命中和代码膨胀。其次,如果循环体内部存在依赖关系,展开循环可能会破坏这些依赖关系,导致错误的结果。因此,在进行Loop unrolling时需要仔细考虑这些因素,并在性能和正确性之间做出权衡。
介绍 two's-complement arithmetic
在计算机科学中,二进制补码算术是一种用于表示和操作有符号整数的方法。它是基于二进制表示法的,其中最高位表示符号位(0表示正数,1表示负数),其余位表示数值部分。
二进制补码算术具有以下两个重要的特性:可交换性和可结合性。
-
可交换性:在二进制补码算术中,加法和乘法操作是可交换的。换句话说,无论操作数的顺序如何,结果都将是相同的。例如,对于任意整数a和b,a + b = b + a和a * b = b * a。
-
可结合性:在二进制补码算术中,加法和乘法操作是可结合的。这意味着,无论操作数的分组方式如何,结果都将是相同的。例如,对于任意整数a、b和c,(a + b) + c = a + (b + c)和(a * b) * c = a * (b * c)。
这些特性使得二进制补码算术在计算机中广泛应用,因为它们使得操作的顺序和分组不影响最终结果。这对于编写和优化代码非常重要,因为它允许编译器和处理器在不改变程序功能的情况下进行优化,提高计算效率。
介绍下 AVX vector instructions
AVX(Advanced Vector Extensions)矢量指令是一组用于x86架构的SIMD(Single Instruction, Multiple Data)指令集扩展。AVX指令集引入了256位宽的矢量寄存器,可以同时处理更多的数据,从而提高程序的并行性和性能。
AVX指令集提供了一些常用的算术和逻辑操作指令,例如加法、减法、乘法、除法、位移、逻辑与/或/非等操作。这些指令可以同时对多个数据进行操作,以实现并行计算。
此外,AVX指令集还提供了一些高级指令,如乘累加指令(FMA,Fused Multiply-Add),可以在一条指令中同时执行乘法和加法操作,进一步提高计算性能。
使用AVX指令集可以充分利用现代处理器中的矢量化单元,提高程序的运行效率。但要注意,为了充分发挥AVX指令集的性能优势,需要编写优化的代码,合理地利用矢量寄存器和矢量化指令。
介绍 Register Spilling
寄存器溢出(Register Spilling)是指在编译器优化过程中,由于寄存器数量有限,无法将所有变量和临时值都存储在寄存器中,因此需要将一些值存储到内存中的过程。
在程序执行过程中,寄存器是最快的存储器,可以直接访问,而内存访问则较慢。因此,尽可能将变量和临时值存储在寄存器中可以提高程序的性能。但是,当寄存器数量不足以容纳所有需要存储的值时,编译器就需要做出决策,将一些值存储到内存中。
寄存器溢出的过程包括以下步骤:
- 分析代码中的变量和临时值的使用情况,确定哪些值需要存储到内存中。
- 为需要存储到内存中的值分配内存空间。
- 在需要使用这些值的地方,将内存中的值加载到寄存器中。
- 在值不再需要使用时,将寄存器中的值存储回内存。
寄存器溢出的过程会引入额外的内存访问开销,因为从内存中加载和存储值比直接在寄存器中操作要慢。因此,编译器会尽量优化寄存器分配,以最小化寄存器溢出的次数和开销。
寄存器溢出是编译器优化的一个重要方面,它可以通过优化寄存器分配算法、减少变量的使用范围等方式来减少溢出的次数,提高程序的性能。
介绍 YMM registers
YMM寄存器是一种特殊的寄存器,它是Intel x86架构中的一部分。YMM寄存器是AVX(Advanced Vector Extensions)指令集的一部分,用于支持向量化操作。
YMM寄存器是256位宽的寄存器,可以存储16个32位整数、16个单精度浮点数或8个双精度浮点数。它们被用于执行SIMD(Single Instruction, Multiple Data)指令,即一条指令可以同时处理多个数据元素。
YMM寄存器的引入使得向量化操作更加高效,可以在单个指令周期内同时处理多个数据,提高程序的并行性和性能。通过将数据存储在YMM寄存器中,可以利用SIMD指令一次性处理多个数据元素,减少了指令的数量和循环迭代次数,从而提高了程序的执行速度。
YMM寄存器的使用需要依赖支持AVX指令集的处理器和相应的编译器支持。在编程中,可以使用特定的指令来操作YMM寄存器,如加载数据、存储数据、执行向量化计算等。
总之,YMM寄存器是一种用于支持向量化操作的特殊寄存器,可以同时处理多个数据元素,提高程序的并行性和性能。它是Intel x86架构中AVX指令集的一部分,需要支持AVX的处理器和编译器来使用。
介绍 conditional move instructions的优点和缺点
条件移动指令(Conditional Move Instructions)是一种在计算机体系结构中用于根据条件选择性地将数据从一个位置移动到另一个位置的指令。它可以避免分支预测失败和流水线中断的问题,从而提高程序的执行效率。
条件移动指令的工作原理是通过将条件表达式直接应用于数据移动操作,而不是通过分支指令来选择执行不同的代码路径。这样,无论条件是否满足,都会执行数据移动操作,避免了分支预测失败的问题。
通过使用条件移动指令,可以避免以下与分支指令相关的问题:
-
分支预测失败:分支指令会根据条件选择执行不同的代码路径,但是分支预测器可能无法准确地预测分支的方向,导致分支预测失败。这会导致流水线中断,浪费了计算资源和时间。条件移动指令避免了分支预测失败,因为它不需要预测分支的方向。
-
流水线中断:分支预测失败会导致流水线中断,因为处理器需要丢弃已经取出的错误分支的指令并重新开始执行正确的分支。这会浪费时间和计算资源。条件移动指令避免了流水线中断,因为它不需要重新开始执行不同的代码路径。
通过使用条件移动指令,可以改善程序的执行效率和性能。然而,需要注意的是,条件移动指令可能会增加代码的复杂性,因为需要手动处理条件和数据移动操作。此外,条件移动指令的效果取决于具体的应用场景和优化目标,因此在使用时需要仔细评估和测试。
总之,条件移动指令是一种可以避免分支预测失败和流水线中断的指令。通过将条件表达式直接应用于数据移动操作,条件移动指令避免了分支预测失败的问题,提高了程序的执行效率和性能。然而,使用条件移动指令需要注意代码复杂性和优化目标。
links
https://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/lectures/10-optimization.pdf