又换工作了, 现在用c/c++。 以后更少有时间在delphi露面了。 看着一堆过去写的delphi代码, 浪费啊,似乎没什么机会用到了。 干脆挑选一下, 以后会不定期发上来给大家分享, 混个脸熟吧。下面实现的sin和cos的原理相当简单, 事先计算好各个角度对应的sin值存在内存表中, 调用函数即是查表, 直接取出对应的值返回即可。 这样用简单的整数地址运算(只牵涉到加减和位运算--大部分都是一个时钟周期一条指令而且可以并行)代替了缓慢的浮点计算,速度当然快多了(题外话,现代CPU的浮点计算并不慢, 因为并行性能好, 在某些情况下甚至比相应的整数运算(整数模拟的定点数)更快, 不过这只是在“某些情况下”, 再加上浮点和整数之间的转换极其耗时, 所以总的来说浮点数运算还是比不上整数的)。为了更进一步提高速度, 我对sin和cos的参数定义做了一点小小的修改, sin和cos接受的参数不是浮点的弧度而是整数的角度(应该更符合普通人的习惯吧),另一点不那么符合大众习惯的是一个圆周不再是360度而是分成了512度(嘿嘿)。 由于现代CPU的特点, 访问的内存数据最好能够常驻L1或L2 cache(访问L1 cache耗时1-3时钟周期, L2 cache在20周期左右, 而访问主内存一般都在200时钟周期以上,这也是过去很多通过查表来替代某些耗时指令的算法今天来运行的话还没有直接使用那个耗时指令来得快的原因),这就要求我们的内存表尽可能小。下面程序中用到的内存表占用516字节,可以很轻松地加载进L1 cache后还剩大把空间供其它程序使用,这使得我们的内存表更容易驻留在L1 cache不被交换出去。表中保存的是1/4圆周的sin值,其它角度都可以通过换算映射到这张表里, 表中每一项是个整数,记录了包括小数点前1位和小数点后30位(二进制)精度的sin值。 代码是用汇编写的,并进行了我力所能及的最大优化:整个函数没有分支跳转指令(如果用delphi写的话代码中必然存在不止一个的if..else结构,要知道现代CPU对分支预测失败的惩罚是相当严厉的,特别是在CPU流水线越来越长的今天,一次分支预测失败耗时在40-200时钟周期,都够让FSINCOS指令完成一大半了),映射角度到1/4圆周的代码,结果正负调整的代码都是通过一些位操作实现的,这就是将圆周定为512度的原因和好处了。具体代码如下:
SinLUT: array [0..128] of Cardinal = ( // Degrees for full circle: 0 -- 512 clockwise
$00000000, $00C90E90, $0192155F, $025B0CAF, $0323ECBE, $03ECADCF, $04B54825, $057DB403,
$0645E9AF, $070DE172, $07D59396, $089CF867, $09640837, $0A2ABB59, $0AF10A22, $0BB6ECEF,
$0C7C5C1E, $0D415013, $0E05C135, $0EC9A7F3, $0F8CFCBE, $104FB80E, $1111D263, $11D3443F,
$1294062F, $135410C3, $14135C94, $14D1E242, $158F9A76, $164C7DDD, $17088531, $17C3A931,
$187DE2A7, $19372A64, $19EF7944, $1AA6C82B, $1B5D100A, $1C1249D8, $1CC66E99, $1D79775C,
$1E2B5D38, $1EDC1953, $1F8BA4DC, $2039F90F, $20E70F32, $2192E09B, $223D66A8, $22E69AC8,
$238E7673, $2434F332, $24DA0A9A, $257DB64C, $261FEFFA, $26C0B162, $275FF452, $27FDB2A7,
$2899E64A, $29348937, $29CD9578, $2A650525, $2AFAD269, $2B8EF77D, $2C216EAA, $2CB2324C,
$2D413CCD, $2DCE88AA, $2E5A1070, $2EE3CEBE, $2F6BBE45, $2FF1D9C7, $30761C18, $30F8801F,
$317900D6, $31F79948, $32744493, $32EEFDEA, $3367C090, $33DE87DE, $34534F41, $34C61236,
$3536CC52, $35A5793C, $361214B0, $367C9A7E, $36E5068A, $374B54CE, $37AF8159, $3811884D,
$387165E3, $38CF1669, $392A9642, $3983E1E8, $39DAF5E8, $3A2FCEE8, $3A8269A3, $3AD2C2E8,
$3B20D79E, $3B6CA4C4, $3BB6276E, $3BFD5CC4, $3C42420A, $3C84D496, $3CC511D9, $3D02F757,
$3D3E82AE, $3D77B192, $3DAE81CF, $3DE2F148, $3E14FDF7, $3E44A5EF, $3E71E759, $3E9CC076,
$3EC52FA0, $3EEB3347, $3F0EC9F5, $3F2FF24A, $3F4EAAFE, $3F6AF2E3, $3F84C8E2, $3F9C2BFB,
$3FB11B48, $3FC395F9, $3FD39B5A, $3FE12ACB, $3FEC43C7, $3FF4E5E0, $3FFB10C1, $3FFEC42D,
$40000000);function sin512(angle: Integer): Single;
asm
// if angle in upper half circle then
// EDX = -1
// map angle to 0..255 (lower half circle)
MOV EDX, 256
AND EDX, EAX
CMP EDX, 1
SBB EDX, EDX
XOR EDX, $FFFFFFFF
AND EAX, 255
// if angle > 128 (right quarter circle) then
// map angle to 0..128 (left quarter circle)
MOV ECX, 128
CMP ECX, EAX
SBB ECX, ECX
XOR EAX, ECX
SUB EAX, ECX
AND ECX, 256
ADD EAX, ECX
// lookup table
MOV EAX, DWORD PTR [EAX*4+SinLUT]
// adjust +/- of result
XOR EAX, EDX
SUB EAX, EDX
// change to float point result
PUSH EAX
FILD DWORD PTR [ESP]
FSTP DWORD PTR [ESP]
// adjust decimal, = value shr 30
SUB DWORD PTR [ESP], $0F000000
FLD DWORD PTR [ESP]
ADD ESP, 4end;function cos512(angle: Integer): Single; // cos(x) = size(x+quarter circle)
asm
ADD EAX, 128
JMP sin512
end;另附计算SinLUT的代码:
type
TDoubleU = record
case Integer of
0: (f: Double);
1: (ul, uh: Cardinal);
end;var
i: Integer;
y: TDoubleU;
r: double;
begin
for i := 0 to 128 do
begin
r := i / 256 * PI;
y.f := sin(r);
y.f := y.f + ldexp(1, 22);
SinLUT[i] := y.ul;
end;
end;
SinLUT: array [0..128] of Cardinal = ( // Degrees for full circle: 0 -- 512 clockwise
$00000000, $00C90E90, $0192155F, $025B0CAF, $0323ECBE, $03ECADCF, $04B54825, $057DB403,
$0645E9AF, $070DE172, $07D59396, $089CF867, $09640837, $0A2ABB59, $0AF10A22, $0BB6ECEF,
$0C7C5C1E, $0D415013, $0E05C135, $0EC9A7F3, $0F8CFCBE, $104FB80E, $1111D263, $11D3443F,
$1294062F, $135410C3, $14135C94, $14D1E242, $158F9A76, $164C7DDD, $17088531, $17C3A931,
$187DE2A7, $19372A64, $19EF7944, $1AA6C82B, $1B5D100A, $1C1249D8, $1CC66E99, $1D79775C,
$1E2B5D38, $1EDC1953, $1F8BA4DC, $2039F90F, $20E70F32, $2192E09B, $223D66A8, $22E69AC8,
$238E7673, $2434F332, $24DA0A9A, $257DB64C, $261FEFFA, $26C0B162, $275FF452, $27FDB2A7,
$2899E64A, $29348937, $29CD9578, $2A650525, $2AFAD269, $2B8EF77D, $2C216EAA, $2CB2324C,
$2D413CCD, $2DCE88AA, $2E5A1070, $2EE3CEBE, $2F6BBE45, $2FF1D9C7, $30761C18, $30F8801F,
$317900D6, $31F79948, $32744493, $32EEFDEA, $3367C090, $33DE87DE, $34534F41, $34C61236,
$3536CC52, $35A5793C, $361214B0, $367C9A7E, $36E5068A, $374B54CE, $37AF8159, $3811884D,
$387165E3, $38CF1669, $392A9642, $3983E1E8, $39DAF5E8, $3A2FCEE8, $3A8269A3, $3AD2C2E8,
$3B20D79E, $3B6CA4C4, $3BB6276E, $3BFD5CC4, $3C42420A, $3C84D496, $3CC511D9, $3D02F757,
$3D3E82AE, $3D77B192, $3DAE81CF, $3DE2F148, $3E14FDF7, $3E44A5EF, $3E71E759, $3E9CC076,
$3EC52FA0, $3EEB3347, $3F0EC9F5, $3F2FF24A, $3F4EAAFE, $3F6AF2E3, $3F84C8E2, $3F9C2BFB,
$3FB11B48, $3FC395F9, $3FD39B5A, $3FE12ACB, $3FEC43C7, $3FF4E5E0, $3FFB10C1, $3FFEC42D,
$40000000);function sin512(angle: Integer): Single;
asm
// if angle in upper half circle then
// EDX = -1
// map angle to 0..255 (lower half circle)
MOV EDX, 256
AND EDX, EAX
CMP EDX, 1
SBB EDX, EDX
XOR EDX, $FFFFFFFF
AND EAX, 255
// if angle > 128 (right quarter circle) then
// map angle to 0..128 (left quarter circle)
MOV ECX, 128
CMP ECX, EAX
SBB ECX, ECX
XOR EAX, ECX
SUB EAX, ECX
AND ECX, 256
ADD EAX, ECX
// lookup table
MOV EAX, DWORD PTR [EAX*4+SinLUT]
// adjust +/- of result
XOR EAX, EDX
SUB EAX, EDX
// change to float point result
PUSH EAX
FILD DWORD PTR [ESP]
FSTP DWORD PTR [ESP]
// adjust decimal, = value shr 30
SUB DWORD PTR [ESP], $0F000000
FLD DWORD PTR [ESP]
ADD ESP, 4end;function cos512(angle: Integer): Single; // cos(x) = size(x+quarter circle)
asm
ADD EAX, 128
JMP sin512
end;另附计算SinLUT的代码:
type
TDoubleU = record
case Integer of
0: (f: Double);
1: (ul, uh: Cardinal);
end;var
i: Integer;
y: TDoubleU;
r: double;
begin
for i := 0 to 128 do
begin
r := i / 256 * PI;
y.f := sin(r);
y.f := y.f + ldexp(1, 22);
SinLUT[i] := y.ul;
end;
end;
procedure TForm2.Button1Click(Sender: TObject);
var i,c:integer;
t:Cardinal;
r1:Single;
r2:Extended;
angle1:Integer;
angle2:Extended;
begin
angle1:=30;
angle2:=30;
c:=1000000; Memo1.Clear; t:=GetTickCount;
for i := 0 to c do
r1:=sin512(angle1);
self.Memo1.Lines.Add(Format('sin512 %d',[GetTickCount-t])) ; t:=GetTickCount;
for i := 0 to c do
r1:=cos512(angle1);
self.Memo1.Lines.Add(Format('cos512 %d',[GetTickCount-t])) ; t:=GetTickCount;
for i := 0 to c do
r1:=sin(angle1);
self.Memo1.Lines.Add(Format('sin %d',[GetTickCount-t])) ; t:=GetTickCount;
for i := 0 to c do
r1:=cos(angle1);
self.Memo1.Lines.Add(Format('cos %d',[GetTickCount-t])) ;
end;测试结果:
1-------------
sin512 15
cos512 16
sin 47
cos 622-------------
sin512 16
cos512 15
sin 47
cos 473--------------
sin512 0
cos512 16
sin 47
cos 62测试环境:
CPU:奔腾 t2130 1.86GHz
内存:1G
windows xp sp3
Delphi2010
sin(x)<>sin512(x)
而double的存储方式本质上是64位整型,运算速度比float慢,存储空间多一倍。所以如果不是对精度有特殊要求,建议应多使用float。
==========================================
这个光整数不行吧?要是几点几度咋整呢?
就是用度分秒,秒也是应该浮点吧?
同样对于角度之间的映射也可以通过查表实现,没必要增加乘法和除法。
carmack在DOOM中的查表法finesine实际使用了10240表项。而且不是说你的L1 data有16K,表有4K,就能保证表在L1 data中,只有单任务系统才可能这么假设(也仅仅是可能)。即便只考虑当前任务,由于一个实际应用软件不可能象简单测试那样疯狂算sin/cos,而不管算出来干什么,它肯定进行一定量(不可能很多)的sin/cos计算后,把结果用于其他的计算过程,比如更新弹道轨迹,复制旋转后的像素等等,而新的计算往往会用到新的数据,结果就是可能把原来cache中的数据冲掉。我估计这就是为什么carmack只在DOS版的DOOM中使用查表法,在Windows版本的DOOM3中用更慢的泰勒级数展开算sin的主要原因(只是我的猜测,精度可能也是一个原因)。至于“同样对于角度之间的映射也可以通过查表实现,没必要增加乘法和除法。”,恐怕也这么简单。
精度高于整数的输入,基本上只能是一个浮点值(否则只能是你强迫用户为了用一个sin/cos函数而改变他的整个坐标系定义,那就本末倒置了),为了查表,需要先乘上一个比例(10或者100之类,取决于要求的精度)再转换成整数,还得做一次mod保证不超出表的范围。