使用 Delphi 多年了,虽然看过 Delphi 中的汇编代码,但基本没有写过汇编代码。因为没有什么项目需求。
今天因为要优化老程序的一段代码,到网上搜索了一下。发现很多都是汇编写的。效率也的确很高。
这让我有了学习汇编的冲动。
汇编代码一般都是在对效率要求比较高的场合才会使用到,比如多媒体、3D、游戏、视频领域。
虽然我不是做相关的行业的。但学习学习总没有坏处。
学习的最好办法是看/写例子。越简单越好。
所以以 RGB2Gray 下手。图片 RGB 转 灰度图。
网络上这样的代码很多,都是用 C/C++ 写的。
我打算用 Delphi 来写。边阅读 C/C++ 代码,边学习。
尽量保持简单。代码少,容易理解。
图片统一解码为 32位,位图格式;JPEG 图片用 GDI+ 解码;因为它效率最好。
测试图片为 4096*4096*32;下面的时间是在我的机器上的运行时间。
Delphi 的 Release 模式是有优化的,Debug 是没有的;下面的时间,都是在我的机器上,DEBUG 模式下的运行用时。
解码图片单元见 db.Image.Load.pas 单元;
GetBitsPointer 等函数(包含知识点:类的私有变量、保护变量的获取)和一些常量的定义都放在 db.Image.Common.pas 单元里面。
灰度变换函数都放在 db.Image.Gray.pas 单元里面;
汇编代码都封装在独立的函数里面。这样很容易扩展至X64平台。
基本原理
Gray = (R + G + B) / 3 = (R + G + B) * 85 div 255 = (R + G + B) * 85 >> 8
Gray = R*0.299 + G*0.587 + B*0.114
Gray = R,G,B 中的最大值
定点优化:
Gray = (R*77 + G*151 + B*28) >> 8
Gray = (R*$4D + G*$97 + B*$1C) >> 8
查表优化:
R、G、B,都在 0---255 之间,可以将 R(0---255)*77、G(0---255)*151、B(0---255)*28 预先计算好,存放在常量表中,优化掉乘法。
灰度值也在 0---255之间,一不做二不休,将求灰度的RGB(Gray, Gray, Gray), 也做成常量表。
RGB2Gray 要实现的方法有很多种。我一一写出来。因为还在学习中,有的暂时还不会写。还望高手指点。
type
TGrayType = (gtAPI, gtScanLine, gtDelphi, gtFourPoint, gtParallel, gtGDIPLUS, gtTable, gtASM, gtMMX, gtSSE, gtAVX, gtAVX512, gtGPU, gtOther);
看字面就很容易理解。API方法、Scanline方法,等等;
函数定义(默认使用 SSE 优化函数,因为现代电脑,估计没有哪个电脑不支持 SSE 指令的了):
procedure Gray(bmp: TBitmap; const gt: TGrayType = gtSSE);
begin
case gt of
gtAPI:
Gray_API(bmp);
gtScanLine:
Gray_ScanLine(bmp);
gtDelphi:
Gray_Delphi(bmp);
gtFourPoint:
Gray_FourPoint(bmp);
gtParallel:
Gray_Parallel(bmp);
gtGDIPLUS:
Gray_GDIPLUS(bmp);
gtTable:
Gray_Table(bmp);
gtASM:
Gray_ASM(bmp);
gtMMX:
Gray_MMX(bmp);
gtSSE:
Gray_SSE(bmp);
gtAVX:
Gray_AVX(bmp);
gtAVX512:
Gray_AVX512(bmp);
gtGPU:
Gray_GPU(bmp);
gtOther:
Gray_Other(bmp);
end;
end;
下面一一实现这些函数。
01、Gray_API(gtAPI):用 WINDOWS API 的方法,获取图像内存区。进行灰度变换。再写回内存区;用时 219 毫秒。
{ 219 ms }
procedure Gray_API(bmp: TBitmap);
var
I, Count: Cardinal;
pColor : PRGBQuad;
byeGray : Byte;
begin
Count := bmp.Width * bmp.Height;
GetMem(pColor, Count * 4);
try
GetBitmapBits(bmp.Handle, Count * 4, pColor);
for I := 0 to Count - 1 do
begin
byeGray := Round(0.299 * pColor^.rgbRed + 0.587 * pColor^.rgbGreen + 0.114 * pColor^.rgbBlue);
pColor^ := TRGBQuad(RGB(byeGray, byeGray, byeGray));
Inc(pColor);
end;
Dec(pColor, Count);
SetBitmapBits(bmp.Handle, Count * 4, pColor);
finally
FreeMem(pColor);
end;
end;
02、Gray_ScanLine(gtScanLine):用 ScanLine 的方法,获取图像内存区。进行灰度变换;想比较于 Gray_API ,优化掉了内存读写和使用了定点优化。速度提高不少。用时 122 毫秒;
{ 122 ms }
procedure Gray_ScanLine(bmp: TBitmap);
var
I, J : Integer;
pColor : PRGBQuad;
byeGray: Byte;
begin
for I := 0 to bmp.Height - 1 do
begin
pColor := bmp.ScanLine[I];
for J := 0 to bmp.Width - 1 do
begin
byeGray := (77 * pColor^.rgbRed + 151 * pColor^.rgbGreen + 28 * pColor^.rgbBlue) shr 8;
pColor^ := TRGBQuad(RGB(byeGray, byeGray, byeGray));
Inc(pColor);
end;
end;
end;
03、Gray_Delphi(gtDelphi):用 Delphi 对象的方法,直接获取图像到内存区。相对于 Gray_Scanline 优化了循环。将两层循环优化成了一层。用时 86 毫秒;
{ 86 ms }
procedure Gray_Delphi(bmp: TBitmap);
var
I, Count: Integer;
pColor : PRGBQuad;
byeGray : Byte;
begin
Count := bmp.Width * bmp.Height;
pColor := GetBitsPointer(bmp);
for I := 0 to Count - 1 do
begin
byeGray := (77 * pColor^.rgbRed + 151 * pColor^.rgbGreen + 28 * pColor^.rgbBlue) shr 8;
pColor^ := TRGBQuad(RGB(byeGray, byeGray, byeGray));
Inc(pColor);
end;
end;
04、Gray_FourPoint(gtFourPoint):同时4点进行运算。效率好像没有多少提高。用时 90 毫秒;
{ 90 ms }
procedure Gray_FourPoint(bmp: TBitmap);
var
I, J, Count: Integer;
pColor : PRGBQuad;
byeGray : Byte;
begin
Count := bmp.Width * bmp.Height;
pColor := GetBitsPointer(bmp);
for I := 0 to Count div 4 - 1 do
begin
for J := 0 to 3 do
begin
byeGray := (77 * pColor^.rgbRed + 151 * pColor^.rgbGreen + 28 * pColor^.rgbBlue) shr 8;
pColor^ := TRGBQuad(RGB(byeGray, byeGray, byeGray));
Inc(pColor);
end;
end;
end;
05、Gray_Parallel(gtParallel):并行优化。注意:需要脱离 IDE 执行 和 ScanLine 不能用于 TParallel.For 中。用时 45 毫秒;
{ 45 ms }
procedure Gray_Parallel(bmp: TBitmap);
var
StartScanLine: Integer;
bmpWidthBytes: Integer;
begin
StartScanLine := Integer(bmp.ScanLine[0]);
bmpWidthBytes := Integer(bmp.ScanLine[1]) - Integer(bmp.ScanLine[0]);
TParallel.For(0, bmp.Height - 1,
procedure(Y: Integer)
var
X: Integer;
pColor: PRGBQuad;
begin
pColor := PRGBQuad(StartScanLine + Y * bmpWidthBytes);
for X := 0 to bmp.Width - 1 do
begin
pColor^ := GetPixelGray(pColor^.rgbRed, pColor^.rgbGreen, pColor^.rgbBlue);
Inc(pColor);
end;
end);
end;
06、Gray_GDIPLUS(gtGDIPLUS):GDI+。用时 1030 毫秒;
{ 1036 ms }
procedure Gray_GDIPLUS(bmp: TBitmap);
var
img: TGPImage;
iab: TGPImageAttributes;
gpg: TGPGraphics;
begin
img := TGPBitmap.Create(bmp.Handle, bmp.Palette);
gpg := TGPGraphics.Create(bmp.Canvas.Handle);
iab := TGPImageAttributes.Create;
try
iab.SetColorMatrix(c_GrayColorMatrix, ColorMatrixFlagsDefault, ColorAdjustTypeBitmap);
gpg.DrawImage(img, TGPRect(bmp.Canvas.ClipRect), 0, 0, bmp.Width, bmp.Height, UnitPixel, iab);
finally
iab.Free;
gpg.Free;
img.Free;
end;
end;
07、Gray_Table(gtTable):查表优化。没有了乘法运算。用时 80 毫秒;(效果一般。看来内存定位速度比CPU寄存器的运算速度慢的多)
{ 80 ms }
procedure Gray_Table(bmp: TBitmap);
var
I, Count: Integer;
pColor : PRGBQuad;
byeGray : Byte;
begin
Count := bmp.Width * bmp.Height;
pColor := GetBitsPointer(bmp);
for I := 0 to Count - 1 do
begin
byeGray := (c_GrayR77[pColor^.rgbRed] + c_GrayG151[pColor^.rgbGreen] + c_GrayB28[pColor^.rgbBlue]) shr 8;
pColor^ := TRGBQuad(c_GrayValue[byeGray]);
Inc(pColor);
end;
end;
08、Gray_ASM(gtASM):将上面的 Gray_Table 改写为汇编代码。效果较好。用时 25 毫秒。(看来 Delphi 的自身编译器优化效果不如人为优化)
procedure Gray_ASM_Proc(pColor: PRGBQuad; const Count: Integer); register;
asm
MOV ECX, EDX // ECX = Count 循环计数 EDX (Count) 赋给 ECX;通常将 ECX 作为计数器来使用。只是约定俗成,不是标准
@LOOP: // 循环;EAX 中存在着 pColor 的首地址
MOVZX EBX, [EAX].TRGBQuad.RGBRed // EBX = pColor^.rgbRed
MOVZX EDX, [EAX].TRGBQuad.rgbGreen // EDX = pColor^.rgbGreen
MOVZX ESI, [EAX].TRGBQuad.rgbBlue // ESI = pColor^.rgbBlue
MOV EBX, [EBX*4 + c_GrayR77] // EBX = c_GrayR77[pColor^.rgbRed]
MOV EDX, [EDX*4 + c_GrayG151] // EDX = c_GrayG151[pColor^.rgbGreen]
MOV ESI, [ESI*4 + c_GrayB28] // ESI = c_GrayB28[pColor^.rgbBlue]
ADD EBX, EDX // EBX = c_GrayR77[pColor^.rgbRed] + c_GrayG151[pColor^.rgbGreen]
ADD EBX, ESI // EBX = c_GrayR77[pColor^.rgbRed] + c_GrayG151[pColor^.rgbGreen] + c_GrayB28[pColor^.rgbBlue]
SHR EBX, 8 // EBX = (c_GrayR77[pColor^.rgbRed] + c_GrayG151[pColor^.rgbGreen] + c_GrayB28[pColor^.rgbBlue]) shr 8;
MOV EBX, [EBX*4 + c_GrayValue] // EBX = TRGBQuad(c_GrayValue[byeGray])
MOV [EAX], EBX // [EAX] = TRGBQuad(c_GrayValue[byeGray])
ADD EAX, 4 // EAX = 指向下一个像素
DEC ECX // Count 减一
JNZ @LOOP // 循环
end;
{ 25 ms }
procedure Gray_ASM(bmp: TBitmap);
begin
Gray_ASM_Proc(GetBitsPointer(bmp), bmp.Width * bmp.Height);
end;
09、Gray_MMX(gtMMX):用 MMX 代码来做乘法运算。每一次只对一个像素进行了灰度处理。效果不是很好。看来要想效果好,必须得多点同时进行运算。用时 27 毫秒。
procedure Gray_MMX_Proc_P1(pColor: PByte; const Count: Integer); register;
asm
EMMS
MOV ECX, EDX // ECX = Count 循环计数 EDX (Count) 赋给 ECX;
PXOR MM7, MM7 // MM7 = $0000000000000000
MOVQ MM6, c_GrayMaskARGB // MM6 = $0000004D0097001C
@LOOP: // 循环;EAX 中存在着 pColor 的首地址
MOVD MM0, [EAX] // MM0 = $0000000000RRGGBB
PUNPCKLBW MM0, MM7 // MM0 = $000000RR00GG00BB
PMADDWD MM0, MM6 // MM0 = A * 0 + R * 77 | G * 151 + B * 28
MOVD EDX, MM0 // EDX = G * 151 + B * 28
PSRLQ MM0, 32 // MM0 = R * 77
MOVD EBX, MM0 // EBX = R * 77
ADD EBX, EDX // EBX = R * 77 + G * 151 + B * 28
SHR EBX, 8 // EBX = (R * 77 + G * 151 + B * 28) >> 8
MOV EBX, [EBX*4 + c_GrayValue] // EBX = TRGBQuad(c_GrayValue[byeGray])
MOV [EAX], EBX // [EAX] = TRGBQuad(c_GrayValue[byeGray])
ADD EAX, 4 // EAX = 指向下一个像素
SUB ECX, 4 // Count 减 4
JNZ @LOOP // 循环
EMMS
end;
{ 27 ms }
procedure Gray_MMX(bmp: TBitmap);
begin
Gray_MMX_Proc_P1(GetBitsPointer(bmp), bmp.Width * bmp.Height * 4);
end;
10、Gray_SSE(gtSSE):用 SSE 代码来做运算。四点同时进行。效果不错。为了熟悉 SSE 指令,先用 (R+G+B) / 3 来取灰度值。用时 17 毫秒。
{
SSE 优化
XMM0-----XMM7 8 个 128bit 寄存器
4个像素可以同时运算
GRAY = (R+G+B) / 3
GRAY = (R+G+B) * 85 / 255
GRAY = (R+G+B) * $55 SHR 8
}
procedure Gray_SSE_Proc_01(pColor: PRGBQuad; const Count: Integer); register;
asm
MOV ECX, EDX
MOVSS XMM1, [c_GraySSEMask] // XMM1 = 000000000000000000000000000000FF
SHUFPS XMM1, XMM1, 0 // XMM1 = 000000FF000000FF000000FF000000FF
MOVSS XMM2, [c_GraySSEDiv3] // XMM2 = 00000000000000000000000000000055
SHUFPS XMM2, XMM2, 0 // XMM2 = 00000055000000550000005500000055
MOVAPS XMM3, XMM1 // XMM3 = 000000FF000000FF000000FF000000FF
PSLLD XMM3, 24 // XMM3 = FF000000FF000000FF000000FF000000
@LOOP:
MOVUPS XMM0, [EAX] // XMM0 = |A3R3G3B3|A2R2G2B2|A1R1G1B1|A0R0G0B0|
MOVAPS XMM4, XMM0 // XMM4 = |A3R3G3B3|A2R2G2B2|A1R1G1B1|A0R0G0B0|
MOVAPS XMM5, XMM0 // XMM5 = |A3R3G3B3|A2R2G2B2|A1R1G1B1|A0R0G0B0|
MOVAPS XMM6, XMM0 // XMM6 = |A3R3G3B3|A2R2G2B2|A1R1G1B1|A0R0G0B0|
// 获取 4 个像素的 B3, B2, B1, B0
ANDPS XMM4, XMM1 // XMM4 = |000000B3|000000B2|000000B1|000000B0|
// 获取 4 个像素的 G3, G2, G1, G0
PSRLD XMM5, 8 // XMM5 = |00A3R3G3|00A2R2G2|00A1R1G1|00A0R0G0|
ANDPS XMM5, XMM1 // XMM5 = |000000G3|000000G2|000000G1|000000G0|
// 获取 4 个像素的 R3, R2, R1, R0
PSRLD XMM6, 16 // XMM6 = |0000A3R3|0000A2R2|0000A1R1|0000A0R0|
ANDPS XMM6, XMM1 // XMM6 = |000000R3|000000R2|000000R1|000000R0|
// 运算
PADDD XMM4, XMM5 // XMM4 = G+B
PADDD XMM4, XMM6 // XMM4 = G+B+R
PMULLW XMM4, XMM2 // XMM4 = (G+B+R) * $55
PSRLD XMM4, 8 // XMM4 = |000000Y3|000000Y2|000000Y1|000000Y0|
// 返回结果
MOVAPS XMM5, XMM4 // XMM5 = |000000Y3|000000Y2|000000Y1|000000Y0|
PSLLD XMM5, 8 // XMM5 = |0000Y300|0000Y200|0000Y100|0000Y000|
ORPS XMM4, XMM5 // XMM4 = |0000Y3Y3|0000Y2Y2|0000Y1Y1|0000Y0Y0|
PSLLD XMM5, 8 // XMM5 = |00Y30000|00Y20000|00Y10000|00Y00000|
ORPS XMM4, XMM5 // XMM4 = |00Y3Y3Y3|00Y2Y2Y2|00Y1Y1Y1|00Y0Y0Y0|
ANDPS XMM0, XMM3 // XMM0 = |FF000000|FF000000|FF000000|FF000000|
ORPS XMM0, XMM4 // XMM0 = |FFY3Y3Y3|FFY2Y2Y2|FFY1Y1Y1|FFY0Y0Y0|
MOVUPS [EAX], XMM0 // [EAX] = XMM0
ADD EAX, 16 // pColor 地址加 16,EAX 指向下4个像素的地址
SUB ECX, 16 // Count 减 16
JNZ @LOOP // 循环
end;
{ 17 ms }
procedure Gray_SSE(bmp: TBitmap);
begin
Gray_SSE_Proc_01(GetBitsPointer(bmp), bmp.Width * bmp.Height * 4);
end;
11、Gray_AVX(gtAVX):用 AVX 代码来实现。
12、Gray_AVX512(gtAVX512):用 AVX512 代码来实现。
13、Gray_GPU(gtGPU):用 GPU 代码来实现。
14、Gray_Other(gtOther):用其它方法来实现。
从几百毫秒到十几毫秒,优化力度可谓不小。花点力气学习也是值得的。
能优化到几毫秒就完美了。见:Delphi 汇编学习(三)--- 图像灰值化的极致优化
详细代码地址:https://github.com/dbyoung720/ImageGray
水平有限,不足的地方还望高手指点。
qq交流群:101611228