[译]译 优化汇编例程(7)

栏目: 编程语言 · 发布时间: 5年前

内容简介:7. 使用汇编器使用固有函数与内联汇编,你可以做的,受到某些限制。这些限制可以通过使用汇编器克服。原则是,编写一个或多个包含程序最关键函数的汇编文件,以C++编写不那么关键的部分。然后将不同的模块链接为一个可执行文件。使用汇编器的好处是:

7. 使用汇编器

使用固有函数与内联汇编,你可以做的,受到某些限制。这些限制可以通过使用汇编器克服。原则是,编写一个或多个包含程序最关键函数的汇编文件,以C++编写不那么关键的部分。然后将不同的模块链接为一个可执行文件。

使用汇编器的好处是:

  • 你可以做的,几乎没有限制。
  • 对最终可执行代码的所有细节,你有完全的控制。
  • 代码的方方面面都可以优化,包括函数prolog与epilog、参数传输方法、寄存器使用、数据对齐等。
  • 制作带有多个入口的函数是可能的。
  • 制作与多个编译器及操作系统兼容的代码是可能的(参考第45页)。
  • MASM与其他某些汇编器有强大的宏语言,它开启了在大多数编译后高级语言中缺失的可能性(参考第111页)。

使用汇编器的坏处是:

  • 汇编语言很难学。有许多指令要记。
  • 以汇编编程比以高级语言编程要花更多时间。
  • 汇编语言语法没有完全标准化。
  • 汇编代码有结构化不良、面条化的倾向。制作结构与可读性良好的汇编代码,需要大量训练。
  • 在不同的微处理器架构间,汇编代码不可移植。
  • 程序员必须知道调用惯例的所有细节,在代码中遵循这些调用惯例。
  • 汇编器提供非常有限的语法检查。许多编程错误汇编器不能查出。
  • 在汇编代码中,你会做错许多事情,这些错误会有严重的后果。
  • 你可能无意间混用VEX与非VEX向量指令。这会导致大的惩罚(参考第13.1节)。
  • 汇编代码中的错误很难追踪。例如,没有保存一个寄存器的错误,会导致程序完全无关的部分出错。
  • 汇编语言不适合制作完成的程序。程序的大部分需要以不同的编程语言来制作。

如果你希望制作汇编代码,开始的最好方式是,首先以C或C++制作整个程序。使用在手册1《优化C++软件》中描述的方法优化程序。如果程序的任一部分需要进一步优化,那么把这部分隔离到一个独立的模块。然后把这个关键模块从C++翻译为汇编。无需手动进行这个翻译。大多数C++编译器都可以产生汇编代码。在把C++代码翻译到汇编时,在编译器中打开所有相关的优化选项。这样,编译器产生的汇编代码是进一步优化的一个良好起点。编译器生成的汇编代码肯定有正确的调用惯例。(由64位编译器对Windows产生的输出,尚未与任何汇编器完全兼容。)

观察由编译器产生的汇编代码,看是否有进一步优化的可能。有时,编译器非常智能,产生的代码要好于汇编 程序员 的平均水平。在其他情形中,编译器笨得难以置信,以非常笨拙、低效的方式做事情。在后者情形中,花时间在汇编编程上是值得的。

大多数IDE(集成开发环境)提供在一个C++项目里包含汇编模块的方法。例如,在Mcirosoft Visual Studio中,你可以对汇编源文件定义一个“定制构建步骤”。例如,这个定制构建步骤可以这样说明。命令行:ml /c /Cx /Zi /coff $(InputName).asm。输出:$(InputName).obj。另外,你可以使用一个makefile(参考第44页)或批文件。

调用汇编模块函数的C++文件应该包含一个包含汇编函数原型的头文件(*.h)。建议向函数原型添加extern “C”,从函数名消除编译器特定的名字重整编码。

不同平台的汇编函数例子在4.5节,第23页给出。

7.1. 静态链接库

从多个汇编文件收集代码到一个函数库是方便的。将汇编模块组织为函数库的好处有:

  • 库可以包含许多函数与模块。链接器将自动挑选一个特定项目所需的模块,不管剩下的,因此没有多余的代码被添加到库里。
  • 函数库很容易、方便地包含进一个C++项目。所有C++编译器与IDE都支持函数库。
  • 函数库是可重用的。如果代码可在不同的项目里重用,花在编写与测试汇编语言函数上的额外时间更为值得。
  • 制作一个可重用库,迫使你做出经过良好测试以及有良好文档,对调用程序具有良好定义功能与接口的代码。
  • 具有通用功能的可重用库,比一个特定于应用、责任不那么良好定义的汇编代码,更容易维护与验证。
  • 函数库可被其他不懂汇编语言的程序员使用。

使用库管理器(即lib.exe)将一个或多个*.obj文件合并为一个*.lib文件,来构建Windows的静态链接库。

使用文档管理器(ar)将一个或多个*.o文件合并为一个*.a文件,来构建 Linux 的静态链接库。

函数库必须辅之以包含库中函数原型的头文件(*.h)。这个头文件包含在调用该库函数的C++文件中(即#include “mylibrary.h”)。

使用makefile(参考第44页)来管理构建与更新函数库所须的指令是方便的。

7.2. 动态链接器

静态链接与动态链接的区别是,静态链接库链接进可执行程序文件,因此可执行文件包含库必须部分的一个拷贝。动态链接库(Windows中*.dll,Linux中*.so)被发布为一个独立文件,在运行时由可执行文件载入。

动态链接库的好处有:

  • 在多个同时运行的程序使用同一个库时,该动态库仅有一个实例被载入内存。
  • 无需修改可执行文件,就可更新动态库。
  • 动态库可被大多数编程语言调用,比如Pascal、C#、Visual Basic(从 Java 调用也是可能的,但困难)。

动态库的坏处有:

  • 即使在仅需要一小部分时,整个库被载入内存。
  • 在可执行程序载入时,载入动态库需要额外的时间。
  • 因为额外的调用开销以及更低效的缓存使用,调用动态库函数效率要低于静态库函数。
  • 动态库必须与可执行文件一起发布。
  • 安装在同一台计算机上的程序必须使用同一个版本的动态库,这会导致许多兼容性问题。

Windows的DLL使用Microsoft链接器(link.exe)制作。必须向链接器提供一个或多个包含所需库函,以及只是返回1的DllEntry函数的.obj或.lib文件。还需要一个模块定义文件(*.def)。注意在32位Windows中,缺省地,DLL函数使用__stdcall调用惯例,而静态库函数使用__cdecl调用惯例。在 www.agner.org/random/randoma.zip 可以找到一个例子源代码。更多指令可以在Microsoft编译器文档以及win32asm.cjb.net处的Iczelion教程里找到。

我没有制作Linux动态库(共享对象)的经验。

7.3. ​​​​​​​源代码形式的库

库形式的子例程库的一个问题是,编译器不能优化函数调用。这个问题可以通过把库函数提供为C++源代码来解决。如果库函数被提供为C++源代码,那么编译器可以通过内联该函数,优化掉函数调用的开销。它可以跨函数优化寄存器分配。它可以执行常量传播。在一个循环里调用这个函数时,它可以移动不会变的代码,等等。

编译器仅能对C++源代码进行这些优化,汇编代码不行。代码可以包含内联汇编或固有函数调用。如果代码使用固有函数调用,编译器可以进行进一步的优化,但使用内联汇编不行。注意,不同的编译器不会同样好地优化代码。

如果编译器使用全程序优化,那么库函数可以作为C++源文件提供。如果不是,那么库代码必须通过#include语句包含,以启用跨函数优化。定义在一个被包含文件里的函数应该被声明为static及/或inline,以避免该函数多个实例间的冲突。

某些有全程序优化特性的编译器可以产生允许在链接阶段进一步优化的部分编译目标文件。不幸,这样文件的格式不是标准化的——甚至在同一个编译器的不同版本间。可能将来编译器技术将考虑标准化的部分编译代码。最低限度,这个格式应该说明对参数传递使用哪些寄存器,每个函数修改了哪些寄存器。在链接时允许跨函数寄存器分配、处理传播、公共子表达式消除,以及不变代码移动会更好。

只要这样的设施不可用,我们可能要考虑使用替换策略,把整个最内层循环放入一个优化的库函数,而不是从一个C++循环里调用这个库函数。这个解决方案用在Intel的Math Kernel Library中(www.intel.com)。例如,如果你需要计算一千个算法,那么你可以提供一个一千个参数的数组给库的一个向量算法函数,并从该库回收一个一千个结果的数组。这有中间结果必须保存在数组,而不是在寄存器中传递的缺点。

7.4. 用汇编制作类

在汇编中,类被编码为结构体,成员函数被编码为接受该类/结构体一个指针为参数的函数。

在C++中,向成员函数应用extern “C”声明是不可能的,因为extern “C”指没有类与成员函数的 C语言 调用惯例。最合逻辑的做法是使用重整函数名。回到例子6.2a与b,第页,我们可以如下使用一个重整名编写成员函数int MyList::Sum():

; Example 7.1a (Example 6.2b translated to stand alone assembly)

; Member function, 32-bit Windows, Microsoft compiler

; Define structure corresponding to class MyList:

MyList STRUC

length_ DD ?                                                    ; length is a reserved name. Use length_

buffer DD 100 DUP (?)                                    ; int buffer[100];

MyList ENDS

; int MyList::Sum()

; Mangled function name compatible with Microsoft compiler (32 bit):

?Sum@MyList@@QAEHXZ PROC near

; Microsoft compiler puts 'this' in ECX

assume ecx: ptr MyList                                   ; ecx points to structure MyList

xor eax, eax                                        ; sum = 0

xor edx, edx                                        ; Loop index i = 0

cmp [ecx].length_, 0                         ; this->length

je L9                                                      ; Skip if length = 0

L1: add eax, [ecx].buffer[edx*4]                    ; sum += buffer[i]

add edx, 1                                            ; i++

cmp edx, [ecx].length_                      ; while (i < length)

jb L1                                                      ; Loop

L9: ret                                                                  ; Return value is in eax

?Sum@MyList@@QAEHXZ ENDP                 ; End of int MyList::Sum()

assume ecx: nothing                                        ; ecx no longer points to anything

重整函数名?Sum@MyList@@QAEHXZ是编译器特定的。其他编译器可能有别的名字重整代码。另外,其他编译器可能将this放在栈上,而不是在一个寄存器中。这些不兼容性可以通过使用一个friend函数,而不是成员函数来解决。这解决了成员函数不能被声明为extern “C”的问题。在C++文件头里的声明必须改变如下:

// Example 7.1b. Member function changed to friend function:

// An incomplete class declaration is needed here:

class MyList;

// Function prototype for friend function with 'this' as parameter:

extern "C" int MyList_Sum(MyList * ThisP);

// Class declaration:

class MyList {

protected:

int length;                               // Data members:

int buffer[100];

public:

MyList();                                 // Constructor

void AttItem(int item);        // Add item to list

// Make MyList_Sum a friend:

friend int MyList_Sum(MyList * ThisP);

// Translate Sum to MyList_Sum by inline call:

int Sum() {return MyList_Sum(this);}

};

友元函数的原型必须在类声明之前出现,因为某些编译器不允许extern “C”在类声明中。一个前向类声明是需要的,因为友元函数需要这个类的一个指针。

上面的声明将使编译器将任何对MyList::Sum的调用替换为对MyList_Sum的调用,因为后者被内联进前者。MyList_Sum的汇编实现不需要重整名:

; Example 7.1c. Friend function, 32-bit mode

; Define structure corresponding to class MyList:

MyList STRUC

length_ DD ?                                                      ; length is a reserved name. Use length_

buffer DD 100 DUP (?)                                     ; int buffer[100];

MyList ENDS

; extern "C" friend int MyList_Sum()

_MyList_Sum PROC near

; Parameter ThisP is on stack

mov ecx, [esp+4]                                     ; ThisP

assume ecx: ptr MyList                                    ; ecx points to structure MyList

xor eax, eax                                              ; sum = 0

xor edx, edx                                              ; Loop index i = 0

cmp [ecx].length_, 0                               ; this->length

je L9                                                           ; Skip if length = 0

L1: add eax, [ecx].buffer[edx*4]                     ; sum += buffer[i]

add edx, 1                                                 ; i++

cmp edx, [ecx].length_                           ; while (i < length)

jb L1                                                           ; Loop

L9: ret                                                                   ; Return value is in eax

_MyList_Sum ENDP                                           ; End of int MyList_Sum()

assume ecx: nothing                                          ; ecx no longer points to anything

7.5. ​​​​​​​线程安全函数

线程安全可重入函数是,在同时从多个线程调用时,正确工作的函数。多线程用于充分利用具有多个CPU核的计算机。因此,要求面向速度关键应用的一个函数库线程安全是合理的。

在除了线程间预期的通讯,线程间没有共享变量时,函数是线程安全的。常量数据可以在线程间共享,没有问题。保存在栈上的变量是线程安全的,因为每个线程有自己的栈。仅对保存在数据段的静态变量,这个问题才出现。在数据必须从一次函数调用保存到下一次时,使用静态变量。制作线程局部静态变量是可能的,但这是低效且系统特定的。

以一个线程安全的方式,从一次函数调用保存数据到下一次最好的方法是,让调用函数对这些数据分配存储空间。这样做最优雅的方式是把数据与需要它们的函数封装在一个类中。每个线程必须创建该类的一个对象,在这个对象上调用成员函数。前面的章节展示了如何以汇编制作成员函数。

如果必须从C或者其他不支持类或不兼容方式支持类的语言调用线程安全汇编函数,解决方案是在每个线程中分配一个储存缓冲,将这个缓冲的指针提供给该函数。

7.6. ​​​​​​​Makefiles

Make应用程序是管理软件项目一个广泛使用的工具。它记录一个软件项目中所有源文件、目标文件、库文件、可执行文件等。它通过基于所有文件的日期/时间戳的一组通用规则来进行。如果一个源文件比对应的目标文件要新,那么这个目标文件必须重新制作。如果目标文件比可执行文件新,那么可执行文件要重新制作。

任何IDE(集成开发环境)包含一个从图形化用户接口激活的make应用程序,但在大多数情形里,使用这个make应用程序的命令行版本也是可能的。命令行make应用程序(称为make或nmake)基于一组你可以定义在所谓的makefile里的规则。使用makefile的好处是,对任意类型文件定义规则是可能的,比如任意编程语言源文件、目标文件、库文件、模块定义文件、资源文件、可执行文件、zip文件等。仅有的要求是,将一个类型文件转换到另一个的 工具 必须存在,而且这个工具可以从命令行调用,文件名作为参数。

对所有伴随Windows与Linux编译器的make应用程序,在makefile中定义规则的语法几乎相同。

许多IDE还支持用于IDE不了解的文件类型的用户定义make规则的特性,但这些应用程序通常不如单独的make应用程序那么通用、灵活。

下面是一个makefile例子,它从3个汇编源文件func1.asm,func2.asm,func3.asm制作函数库mylibrary.lib,并与对应的头文件mylibrary.h一起封装到zip文件mylibrary.zip。

# Example 7.2. makefile for mylibrary

mylibrary.zip: mylibrary.lib mylibrary.h

wzzip $@ $?

mylibrary.lib: func1.obj func2.obj func3.obj

lib /out:$@ $**

.asm.obj

ml /c /Cx /coff /Fo$@ $*.asm

行mylibrary.zip: mylibrary.lib mylibrary.h说明文件mylibrary.zip从mylibrary.lib与mylibrary.h构建,如果这些中任一个比zip文件新,zip文件必须重新构建。下一行,必须缩进,说明从依赖mylibrary.lib与mylibrary.h构建目标文件mylibrary.zip所需的命令(wzzip是WinZip的一个命令行版本)。下两行说明如何从三个目标文件构建库文件mylibrary.lib。行.asm.obj是一个通用规则。它说明任何带有.obj扩展名的文件可以通过下面的缩进行,从具有同名及扩展名.asm的文件构建。

构建规则可以使用以下宏说明文件名:

$@ = 当前目标全名

$< = 依赖文件的全名

$* = 当前目标不带扩展名的基本名

$** = 当前目标的所有依赖,空格分开(nmake)

$+ =当前目标的所有依赖,空格分开(Gnu make)

$? = 时间戳比目前标签新的所有依赖

命令nmake /Fmakefile或make -f makefile激活make应用程序。细节参考特定make应用程序手册。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

垃圾回收算法手册:自动内存管理的艺术

垃圾回收算法手册:自动内存管理的艺术

Richard Jones、Eliot Moss、Antony Hosking / 王雅光、薛迪 / 机械工业出版社 / 2016-3 / 139

在自动内存管理领域,Richard Jones于1996年出版的《Garbage Collection:Algorithms for Automatic Dynamic Memory Management》可谓是一部里程碑式的作品。接近20年过去了,垃圾回收技术得到了非常大的发展,因此有必要将该领域当前最先进的技术呈现给读者。本书汇集了自动内存管理研究者和开发者们在过去50年间的丰富经验,在本书中......一起来看看 《垃圾回收算法手册:自动内存管理的艺术》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具