是否有更快的方法将大位图旋转90或270度而不是简单地执行具有倒置坐标的嵌套循环?
位图是8bpp,通常是2048 * 2400 * 8bpp
目前我通过简单地使用参数反转进行复制(伪代码:
)for x = 0 to 2048-1
for y = 0 to 2048-1
dest[x][y]=src[y][x];
(实际上我是用指针做的,速度要快一点,但这个数字大致相同)
对于大图像,GDI非常慢,纹理(GF7卡)的GPU加载/存储时间与当前CPU时间的大小相同。
任何提示,指示?就地算法甚至会更好,但速度比就地更重要。
目标是Delphi,但它更像是一个算法问题。 SSE(2)矢量化没问题,对于我来说,在汇编程序中编写它是一个很大的问题
跟进Nils的回答
旧例程的时间:32ms(步骤1)
步长8:12ms的时间
步长16:10ms的时间
步长32+:9ms
的时间与此同时,我还在Athlon 64 X2(5200+ iirc)上进行了测试,其速度略高于四倍(80到19毫秒)。
加速非常值得,谢谢。也许在夏季我会用SSE(2)版本折磨自己。但是我已经考虑过如何解决这个问题了,我想我会用完SSE2寄存器来实现直接实现:
for n:=0 to 7 do
begin
load r0, <source+n*rowsize>
shift byte from r0 into r1
shift byte from r0 into r2
..
shift byte from r0 into r8
end;
store r1, <target>
store r2, <target+1*<rowsize>
..
store r8, <target+7*<rowsize>
所以8x8需要9个寄存器,但32位SSE只有8个。无论如何这是夏季的事情: - )
请注意,指针的东西是我本能地做的事情,但实际上可能存在某些东西,如果你的尺寸没有硬编码,编译器就无法将mul变成一个移位。虽然现在muls很便宜,但它们也会产生更多的注册压力。
代码(通过从“naieve”rotate1实现中减去结果来验证):
const stepsize = 32;
procedure rotatealign(Source: tbw8image; Target:tbw8image);
var stepsx,stepsy,restx,resty : Integer;
RowPitchSource, RowPitchTarget : Integer;
pSource, pTarget,ps1,ps2 : pchar;
x,y,i,j: integer;
rpstep : integer;
begin
RowPitchSource := source.RowPitch; // bytes to jump to next line. Can be negative (includes alignment)
RowPitchTarget := target.RowPitch; rpstep:=RowPitchTarget*stepsize;
stepsx:=source.ImageWidth div stepsize;
stepsy:=source.ImageHeight div stepsize;
// check if mod 16=0 here for both dimensions, if so -> SSE2.
for y := 0 to stepsy - 1 do
begin
psource:=source.GetImagePointer(0,y*stepsize); // gets pointer to pixel x,y
ptarget:=Target.GetImagePointer(target.imagewidth-(y+1)*stepsize,0);
for x := 0 to stepsx - 1 do
begin
for i := 0 to stepsize - 1 do
begin
ps1:=@psource[rowpitchsource*i]; // ( 0,i)
ps2:=@ptarget[stepsize-1-i]; // (maxx-i,0);
for j := 0 to stepsize - 1 do
begin
ps2[0]:=ps1[j];
inc(ps2,RowPitchTarget);
end;
end;
inc(psource,stepsize);
inc(ptarget,rpstep);
end;
end;
// 3 more areas to do, with dimensions
// - stepsy*stepsize * restx // right most column of restx width
// - stepsx*stepsize * resty // bottom row with resty height
// - restx*resty // bottom-right rectangle.
restx:=source.ImageWidth mod stepsize; // typically zero because width is
// typically 1024 or 2048
resty:=source.Imageheight mod stepsize;
if restx>0 then
begin
// one loop less, since we know this fits in one line of "blocks"
psource:=source.GetImagePointer(source.ImageWidth-restx,0); // gets pointer to pixel x,y
ptarget:=Target.GetImagePointer(Target.imagewidth-stepsize,Target.imageheight-restx);
for y := 0 to stepsy - 1 do
begin
for i := 0 to stepsize - 1 do
begin
ps1:=@psource[rowpitchsource*i]; // ( 0,i)
ps2:=@ptarget[stepsize-1-i]; // (maxx-i,0);
for j := 0 to restx - 1 do
begin
ps2[0]:=ps1[j];
inc(ps2,RowPitchTarget);
end;
end;
inc(psource,stepsize*RowPitchSource);
dec(ptarget,stepsize);
end;
end;
if resty>0 then
begin
// one loop less, since we know this fits in one line of "blocks"
psource:=source.GetImagePointer(0,source.ImageHeight-resty); // gets pointer to pixel x,y
ptarget:=Target.GetImagePointer(0,0);
for x := 0 to stepsx - 1 do
begin
for i := 0 to resty- 1 do
begin
ps1:=@psource[rowpitchsource*i]; // ( 0,i)
ps2:=@ptarget[resty-1-i]; // (maxx-i,0);
for j := 0 to stepsize - 1 do
begin
ps2[0]:=ps1[j];
inc(ps2,RowPitchTarget);
end;
end;
inc(psource,stepsize);
inc(ptarget,rpstep);
end;
end;
if (resty>0) and (restx>0) then
begin
// another loop less, since only one block
psource:=source.GetImagePointer(source.ImageWidth-restx,source.ImageHeight-resty); // gets pointer to pixel x,y
ptarget:=Target.GetImagePointer(0,target.ImageHeight-restx);
for i := 0 to resty- 1 do
begin
ps1:=@psource[rowpitchsource*i]; // ( 0,i)
ps2:=@ptarget[resty-1-i]; // (maxx-i,0);
for j := 0 to restx - 1 do
begin
ps2[0]:=ps1[j];
inc(ps2,RowPitchTarget);
end;
end;
end;
end;
更新2个泛型
我尝试将此代码更新为Delphi XE中的泛型版本。我因为QC 99703而失败了,论坛的人已经确认它也存在于XE2中。请投票给它:-)
更新3个泛型 现在可以在XE10中使用
答案 0 :(得分:21)
是的,有更快的方法可以做到这一点。
您的简单循环大部分时间都在缓存未命中中。发生这种情况是因为您在紧密循环中触摸了很多不同位置的大量数据。更糟糕的是:你的记忆位置恰好相差两个。这是缓存执行最差的大小。
如果改善内存访问的位置,则可以改进此旋转算法。
一种简单的方法是使用您用于整个位图的相同代码自动旋转每个8x8像素块,并包装另一个循环,将图像旋转分成每个8x8像素的块。 / p>
E.g。这样的事情(没有检查,对不起C代码。我的Delphi技能不是最新的):
// this is the outer-loop that breaks your image rotation
// into chunks of 8x8 pixels each:
for (int block_x = 0; block_x < 2048; block_x+=8)
{
for (int block_y = 0; blocky_y < 2048; block_y+=8)
{
// this is the inner-loop that processes a block
// of 8x8 pixels.
for (int x= 0; x<8; x++)
for (int y=0; y<8; y++)
dest[x+block_x][y+block_y] = src[y+block_y][x+block_x]
}
}
还有其他方法。您可以使用Hilbert-Order或Morton-Order处理数据。理论上这甚至会更快一点,但代码会复杂得多。
顺便说一下 - 既然你提到SSE是你的选择。请注意,您可以在SSE寄存器中旋转8x8字节块。让它工作有点棘手,但是看看SSE矩阵转置代码应该让你开始,因为它是一样的。
修改强>
刚检查过:
块大小为8x8像素,代码运行大约。在我的机器上快5倍。块大小为16x16,运行速度提高了10倍。
似乎尝试使用不同的块大小是个好主意。
这是我用过的(非常简单的)测试程序:
#include <stdio.h>
#include <windows.h>
char temp1[2048*2048];
char temp2[2048*2048];
void rotate1 (void)
{
int x,y;
for (y=0; y<2048; y++)
for (x=0; x<2048; x++)
temp2[2048*y+x] = temp1[2048*x+y];
}
void rotate2 (void)
{
int x,y;
int bx, by;
for (by=0; by<2048; by+=8)
for (bx=0; bx<2048; bx+=8)
for (y=0; y<8; y++)
for (x=0; x<8; x++)
temp2[2048*(y+by)+x+bx] = temp1[2048*(x+bx)+y+by];
}
void rotate3 (void)
{
int x,y;
int bx, by;
for (by=0; by<2048; by+=16)
for (bx=0; bx<2048; bx+=16)
for (y=0; y<16; y++)
for (x=0; x<16; x++)
temp2[2048*(y+by)+x+bx] = temp1[2048*(x+bx)+y+by];
}
int main (int argc, char **args)
{
int i, t1;
t1 = GetTickCount();
for (i=0; i<20; i++) rotate1();
printf ("%d\n", GetTickCount()-t1);
t1 = GetTickCount();
for (i=0; i<20; i++) rotate2();
printf ("%d\n", GetTickCount()-t1);
t1 = GetTickCount();
for (i=0; i<20; i++) rotate3();
printf ("%d\n", GetTickCount()-t1);
}
答案 1 :(得分:3)
如果您可以使用C ++,那么您可能需要查看Eigen。
它是一个C ++模板库,它使用 SSE(2及更高版本)和AltiVec指令集,优雅地回退到非向量化代码。
快速。 (见基准) 表达式模板允许智能地移除临时值并启用延迟评估,在适当的时候 - 在大多数情况下,Eigen会自动处理这种情况并处理混叠。
对SSE(2和更高版本)和AltiVec指令集执行显式矢量化,优雅地回退到非矢量化代码。表达式模板允许对整个表达式全局执行这些优化 对于固定大小的对象,可以避免动态内存分配,并在有意义时展开循环 对于大型矩阵,特别注意缓存友好性。
答案 2 :(得分:0)
你可能能够通过复制缓存对齐的块而不是行来改进它,因为当src dest的步幅将是一个未命中时(取决于delphi是否是行主要或专栏)。
答案 3 :(得分:0)
如果图像不是正方形,则无法就地进行。即使你在方形图像中工作,变换也不利于就地工作。
如果你想尝试更快一些事情,你可以尝试利用行步长来使其工作,但我认为你要做的最好的事情是一次读取4个字节。源,然后将其写入dest中的四个连续行。这应该减少一些开销,但我不会期望超过5%的改善。