入门级的编程书籍一般都介绍“华氏温度转换为摄氏温度”的例子。
从华氏度转换成摄氏度的计算公式为
笔者对程序添加了简单的出错处理:
① 输入的温度数必须正确。
② 核查最终结果,确保不会出现绝对零度(−273℃)以下的摄氏温度值。这是中学物理学课本介绍过的一个常识。
说明:调用函数exit()会立即退出本程序,而且不会向调用方函数返回任何值。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int celsius, fahr;
printf ("Enter temperature in Fahrenheit:\n");
if (scanf ("%d", &fahr)!=1)
{
printf ("Error while parsing your input\n");
exit(0);
};
celsius = 5 * (fahr-32) / 9;
if (celsius<-273)
{
printf ("Error: incorrect temperature!\n");
exit(0);
};
printf ("Celsius: %d\n", celsius);
};
指令清单35.1 x86构架下MSVC 2012优化
$SG4228 DB 'Enter temperature in Fahrenheit:', 0aH, 00H
$SG4230 DB '%d', 00H
$SG4231 DB 'Error while parsing your input', 0aH, 00H
$SG4233 DB 'Error: incorrect temperature!', 0aH, 00H
$SG4234 DB 'Celsius: %d', 0aH, 00H
_fahr$ = -4 ; size = 4
_main PROC
push ecx
push esi
mov esi, DWORD PTR __imp__printf
push OFFSET $SG4228 ; 'Enter temperature in Fahrenheit:'
call esi ; call printf()
lea eax, DWORD PTR _fahr$[esp+12]
push eax
push OFFSET $SG4230 ; '%d'
call DWORD PTR __imp__scanf
add esp, 12 ; 0000000cH
cmp eax, 1
je SHORT $LN2@main
push OFFSET $SG4231 ; 'Error while parsing your input'
call esi ; call printf()
add esp, 4
push 0
call DWORD PTR __imp__exit
$LN9@main:
$LN2@main:
mov eax, DWORD PTR _fahr$[esp+8]
add eax, -32 ; ffffffe0H
lea ecx, DWORD PTR [eax+eax*4]
mov eax, 954437177 ; 38e38e39H
imul ecx
sar edx, 1
mov eax, edx
shr eax, 31 ; 0000001fH
add eax, edx
cmp eax, -273 ; fffffeefH
jge SHORT $LN1@main
push OFFSET $SG4233 ; 'Error: incorrect temperature!'
call esi ; call printf()
add esp, 4
push 0
call DWORD PTR __imp__exit
$LN10@main:
$LN1@main:
push eax
push OFFSET $SG4234 ; 'Celsius: %d'
call esi ; call printf()
add esp, 8
; return 0 - by C99 standard
xor eax, eax
pop esi
pop ecx
ret 0
$LN8@main:
_main ENDP
必须说明的是:
程序首先把printf()函数的内存地址保存到ESI寄存器。在此之后,只要调用“CALL ESI”指令即可调用prinf()函数了。这是非常常见的编译技术,或许是为了方便后续程序频繁调用这个函数,或许是因为还有“不用白不用”的空闲寄存器。
使用加法ADD而不使用减法SUB。我们注意到程序中有一行指令是“ADD EAX,−32”,它用来实现从EAX寄存器中减去32的目的。程序没有采用指令“SUB EAX,32”,也就是说程序使用了EAX=EAX+(−32)的算法,而没采用EAX=EAX−32的算法。是不是值得这么做,笔者并不能完全确定。
为了实现“乘以5”的运算而使用了LEA指令:“lea ecx,DWORD PTR[eax+eax*4]”使得“i+i*4”和“i*5”是相等的。但是指令LEA运行速度比IMUL快。另外还可以使用指令对——SHL EAX,2和ADD EAX,EAX来代替。确实有些编译器是这样做的。
这里也用到了用乘法来代替除法的技巧。参见第41章。
如果主函数main()没有明确的返回值,在程序退出时,它的返回值为0。C99标准中标明“如果main()函数没有通过明确的return指令声明其返回值,那么它将默认返回0”。当然这项规则仅仅适用于主函数main()。虽然MSVC并未声称它完全遵循C99标准,但是或许它部分遵循了这一标准吧。这里指的C99标准是ISO07,P.5.1.2.2.3。
x64的代码和x86的代码大体相同。只是每次调用exit()函数之后,都有一个INT 3指令。
xor ecx, ecx
call QWORD PTR __imp_exit
int 3
INT 3是调试器debugger的断点设置指令。
当程序执行exit()函数之后,它就不会再返回到原程序,而是直接退出了。编译器大概认为,在发生异常退出的情况下,通常人们应当使用调试器分析异常情况吧。
#include <stdio.h>
#include <stdlib.h>
int main()
{
double celsius, fahr;
printf ("Enter temperature in Fahrenheit:\n");
if (scanf ("%lf", &fahr)!=1)
{
printf ("Error while parsing your input\n");
exit(0);
};
celsius = 5 * (fahr-32) / 9;
if (celsius<-273)
{
printf ("Error: incorrect temperature!\n");
exit(0);
};
printf ("Celsius: %lf\n", celsius);
};
MSVC 2010 x86采用的是FPU指令。
指令清单35.2 MSVC 2010 x86优化
$SG4038 DB 'Enter temperature in Fahrenheit:', 0aH, 00H
$SG4040 DB '%lf', 00H
$SG4041 DB 'Error while parsing your input', 0aH, 00H
$SG4043 DB 'Error: incorrect temperature!', 0aH, 00H
$SG4044 DB 'Celsius: %lf', 0aH, 00H
__real@c071100000000000 DQ 0c071100000000000r ; -273
__real@4022000000000000 DQ 04022000000000000r ; 9
__real@4014000000000000 DQ 04014000000000000r ; 5
__real@4040000000000000 DQ 04040000000000000r ; 32
_fahr$ = -8 ; size = 8
_main PROC
sub esp, 8
push esi
mov esi, DWORD PTR __imp__printf
push OFFSET $SG4038 ; 'Enter temperature in Fahrenheit:'
call esi ; call printf()
lea eax, DWORD PTR _fahr$[esp+16]
push eax
push OFFSET $SG4040 ; '%lf'
call DWORD PTR __imp__scanf
add esp, 12 ; 0000000cH
cmp eax, 1
je SHORT $LN2@main
push OFFSET $SG4041 ; 'Error while parsing your input'
call esi ; call printf()
add esp, 4
push 0
call DWORD PTR __imp__exit
$LN2@main:
fld QWORD PTR _fahr$[esp+12]
fsub QWORD PTR __real@4040000000000000 ; 32
fmul QWORD PTR __real@4014000000000000 ; 5
fdiv QWORD PTR __real@4022000000000000 ; 9
fld QWORD PTR __real@c071100000000000 ; -273
fcomp ST(1)
fnstsw ax
test ah, 65 ; 00000041H
jne SHORT $LN1@main
push OFFSET $SG4043 ; 'Error: incorrect temperature!'
fstp ST(0)
call esi ; call printf()
add esp, 4
push 0
call DWORD PTR __imp__exit
$LN1@main:
sub esp, 8
fstp QWORD PTR [esp]
push OFFSET $SG4044 ; 'Celsius: %lf'
call esi
add esp, 12 ; 0000000cH
; return 0 - by C99 standard
xor eax, eax
pop esi
add esp, 8
ret 0
$LN10@main:
_main ENDP
但MSVC 2012分配的却是SIMD指令。
指令清单35.3 MSVC 2012 x86优化
$SG4228 DB 'Enter temperature in Fahrenheit:', 0aH, 00H
$SG4230 DB '%lf', 00H
$SG4231 DB 'Error while parsing your input', 0aH, 00H
$SG4233 DB 'Error: incorrect temperature!', 0aH, 00H
$SG4234 DB 'Celsius: %lf', 0aH, 00H
__real@c071100000000000 DQ 0c071100000000000r ; -273
__real@4040000000000000 DQ 04040000000000000r ; 32
__real@4022000000000000 DQ 04022000000000000r ; 9
__real@4014000000000000 DQ 04014000000000000r ; 5
_fahr$ = -8 ; size = 8
_main PROC
sub esp, 8
push esi
mov esi, DWORD PTR __imp__printf
push OFFSET $SG4228 ; 'Enter temperature in Fahrenheit:'
call esi ; call printf()
lea eax, DWORD PTR _fahr$[esp+16]
push eax
push OFFSET $SG4230 ; '%lf'
call DWORD PTR __imp__scanf
add esp, 12 ; 0000000cH
cmp eax, 1
je SHORT $LN2@main
push OFFSET $SG4231 ; 'Error while parsing your input'
call esi ; call printf()
add esp, 4
push 0
call DWORD PTR __imp__exit
$LN9@main:
$LN2@main:
movsd xmm1, QWORD PTR _fahr$[esp+12]
subsd xmm1, QWORD PTR __real@4040000000000000 ; 32
movsd xmm0, QWORD PTR __real@c071100000000000 ; -273
mulsd xmm1, QWORD PTR __real@4014000000000000 ; 5
divsd xmm1, QWORD PTR __real@4022000000000000 ; 9
comisd xmm0, xmm1
jbe SHORT $LN1@main
push OFFSET $SG4233 ; 'Error: incorrect temperature!'
call esi ; call printf()
add esp, 4
push 0
call DWORD PTR __imp__exit
$LN10@main:
$LN1@main:
sub esp, 8
movsd QWORD PTR [esp], xmm1
push OFFSET $SG4234 ; 'Celsius: %lf'
call esi ; call printf()
add esp, 12 ; 0000000cH
; return 0 - by C99 standard
xor eax, eax
pop esi
add esp, 8
ret 0
$LN8@main:
_main ENDP
当然,x86的指令集确实支持SIMD指令,浮点数运算也毫无问题。大概是这种方式的计算指令比较简单,所以微软的编译器分配了SIMD指令。
我们还注意到绝对零度−273,早早地就导入了寄存器XMM0。这也没关系,编译器不是按照源代码的书写顺序分配的汇编指令。