所谓并行计算,可以让一段代码让 CPU 的多个核同时开跑,非常明显地提高代码执行速度。
所谓“程序”,这个中文单词,严格意义上来说,就是按照特定顺序,一步一步地执行一些指令。这是标准的串行计算。串行计算的好处是有上下文依赖关系的事情,不会搞错顺序。好比先洗碗,再打饭,程序这样写了,计算机绝对不会搞错成先打了饭吃完了才发现碗先没洗导致肚子疼。
但很多时候,我们有大量数据需要计算,并且数据之间没有前后依赖关系。比如:图片处理。我需要逐个像素处理,但处理第一个像素和处理第二个像素,没有依赖关系。处理第一行和处理第二行,也没有依赖关系。这时候,可以同时并行地处理。传统做法是,我们写一个循环,一行一行的逐行处理;在一行中,我们写个循环,一个像素一个像素地逐个处理。最终结果就是一张图片,我们是逐个像素地处理。如果图片有 100 万个像素,就要循环 100 万次。很消耗时间。
现代的 CPU,都是多核的。一个循环跑下来,只使用了一个核。其它几个核都空闲着。并行计算的概念就是,把上面说的那种没有先后依赖关系的数据处理,分到几个核里面同时处理。就好比有 12 个碗要洗,找一个人来一只一只地洗,需要12分钟;找4个人来一起洗,每个人只需要洗三个碗,洗完 12 个碗只需要3分钟。
Delphi 已经支持并行计算。语法上,该怎么写呢?我们来看一个例子。这个例子的代码,是我自己写了以后测试过的(在英巴的官方论坛上请教了一下,得到 Delphi 官方人员的帮助下写出来的代码)。测试程序是一个跑在手机里的 APP,测试 Delphi 的并行计算代码跑安卓手机上面,是否能利用手机 CPU 的多个核。这个代码完成一个图片逐个像素的处理:将 RGB565 格式的图片转为 RGB888 的图片,每个像素单独计算。注意这是 Delphi 写的手机程序,是基于 FireMonkey 框架的,里面的 TBitmap 和 VCL 底下的 TBitmap 有点不一样。
看代码,第一个函数,对图片像素的一行,做一个循环,将一行里面的每个像素拿出来,做一次转换计算:
//这是计算一行的颜色值。其中 InputPtr 是 RGB565 的数据在内存里面的指针;ColorPtr 是 TBitmap 的 TBitmapData 的指针。
//BTW: 以下代码有点问题:颜色值可能搞错,出来的结果,灰色正确,黄色变成了蓝色。图像清晰度没问题。
procedure TForm1.ConvertOneLine(const BmpWidth: Integer; InputPtr: PWord;
ColorPtr: PByte);
var
Col: Integer;
Color: Word;
begin
for Col := 1 to BmpWidth do
begin
Color := InputPtr^;
Inc(InputPtr);
ColorPtr^ := (( Color and $1F) * $FF) div $1F;
Inc(ColorPtr);
ColorPtr^ := (((Color shr 5) and $3F) * $FF) div $3F;
Inc(ColorPtr);
ColorPtr^ := (((Color shr 11) and $1F) * $FF) div $1F;
Inc(ColorPtr);
ColorPtr^ := $FF;
Inc(ColorPtr);;
end;
end;
第二个函数,是对图片的行数,做一个循环,逐行处理。这个函数里面调用上面那个一行里面逐点处理的方法。实际上就是两层循环:
//以下代码,有串行计算的代码,也有并行计算的代码,都测试通过。
procedure TForm1.ConvertRGB565ToRGB8888(const BmpWidth, BmpHeight: Integer; RGB565DataPtr: PWord; BmpData: TBitmapData);
var
InputPitch: Integer;
Row, Col: Integer;
Color: Word;
ColorPtr: PByte;
InputPtr: PWord; // Pointer to RGB565 data
begin
{------------------------------------------------------------------------
把 RGB565 的数据转换到 FireMonkey 的 TBitmap 里面去。算法来自 Delphi 官方网站论坛里面的一个程序员对我的问题的回复。
------------------------------------------------------------------------}
InputPitch:= BmpWidth*2;
//将下面的串行循环,改为并行循环,在4核手机上测试通过。
//大概比串行循环的时间少一半(并没有少 1/4)。
TParallel.For(1, BmpHeight, procedure(Row: Integer)
begin
InputPtr:= PWord(PByte(RGB565DataPtr) + (Row-1)*InputPitch);
ColorPtr:= PByte(BmpData.Data) + (BmpHeight-Row)*BmpData.Pitch;
Self.ConvertOneLine(BmpWidth, InputPtr, ColorPtr);
end
);
{ 测试结果:以下串行计算代码执行正确。
for Row := 1 to BmpHeight do
begin
InputPtr:= PWord(PByte(RGB565DataPtr) + (Row-1)*InputPitch);
ColorPtr:= PByte(BmpData.Data) + (BmpHeight-Row)*BmpData.Pitch;
Self.ConvertOneLine(BmpWidth, InputPtr, ColorPtr);
//TParallel.&For
end;
}
end;
下面是主程序,输入一张图片,调用上面那个函数,完成转换:
//以下代码是调用 ConvertRGB565ToRGB8888 的主程序:
procedure TForm1.DrawRGB656OntoBMP;
var
//直接将 RGB656 的数据画到 BITMAP 上去试试看:
ABitmap: TBitmap;
BmpData: TBitmapData;
BmpWidth: Integer;
BmpHeight: Integer;
RGB565DataPtr, InputPtr: PWord; // Pointer to RGB565 data
AMemoryStream: TMemoryStream;
Fn: string;
T: TDateTime;
InputPitch: Integer;
Row, Col: Integer;
Color: Word;
ColorPtr: PByte;
begin
// 这段代码的测试结果:1. 在安卓下,直接出现浮点运算异常错误;
//2. 在 WINDOWS 底下,执行完成,但没有图像显示。并且转换过程耗时 200MS.跟踪 ScanlineToAlphaColor 函数内部,实际上是逐个点转换为 RGB888
Fn := Memo1.Lines.Strings[0];
AMemoryStream := TMemoryStream.Create;
AMemoryStream.LoadFromFile(Fn);
try
//ABitmap := Image1.Bitmap;
BmpWidth:= 1280;
BmpHeight:= 720;
Image1.Bitmap.SetSize(BmpWidth, BmpHeight);
RGB565DataPtr := PWord(NativeInt(AMemoryStream.Memory) + 66);
InputPitch:= BmpWidth*2; // Bytes by row
T := Now;
if Image1.Bitmap.Map(TMapAccess.Write, BmpData) then //Map 方法取得 Bitmap 内部的数据结构,然后才能直接操作数据结构内部的指针。
try
Self.ConvertRGB565ToRGB8888(BmpWidth, BmpHeight, RGB565DataPtr, BmpData);
finally
Image1.Bitmap.Unmap(BmpData);
Image1.UpdateEffects;
end;
Memo1.Lines.Add('time consuming = ' + IntToStr(MilliSecondsBetween(Now, T)));
finally
AMemoryStream.Free;
end;
end;
测试结果:
对720P 的 RGB565 转码为 RGBA888,串行计算在 Find5 手机上耗时58MS,并行计算耗时 32MS。
结论:Delphi 的并行计算代码,确实可以让多个核同时跑起来,缩短计算时间。