SetPixel 会随着时间的推移而变慢

Posted

技术标签:

【中文标题】SetPixel 会随着时间的推移而变慢【英文标题】:SetPixel slows down over time 【发布时间】:2014-02-04 07:02:14 【问题描述】:

我喜欢在DesktopWindow 上使用SetPixel,但有时它的行为很奇怪。

for(i=0;i<10000;i++)
    SetPixel(DC,100+100*sin((float)i/100),100+100*cos((float)i/100),0);

上面的代码应该打印 10,000 像素,在屏幕的左上角绘制一个圆形。但是如果我多次使用它,它会变得越来越慢。下面的代码应该提供一个这样的例子:

#include<windows.h>
int main()
    Sleep(4000);//waiting you to be ready
    int i,j,k,l;
    HDC DC=GetDC(GetDesktopWindow());
    j=GetTickCount();//base time
    for(l=0;l<10;l++)
    
        for(i=0;i<10000;i++)
            SetPixel(DC,rand()%1000,rand()%1000,0);//print 10000 random x,y pixel
        printf("%d\n",(k=GetTickCount())-j);//time duration from the last count
        for(i=0;i<10000;i++)
            SetPixel(DC,rand()%1000,rand()%1000,0);
        printf("%d\n",(j=GetTickCount())-k);
    
    return 0;

为什么这个操作会随着时间的推移而变慢?

【问题讨论】:

printf 是时间的一部分,所以可能就是这样。尝试将其注释掉,看看它是否看起来仍然变慢。 SetPixel 从来都不是在显示器上绘图的最快方式。 是的。 SetPixel 的效率如此之低,以至于处理器降低时钟频率以保持散热已经是一个简单的解释。 @HansPassant 如果GetTickCount函数返回时钟周期数,这不会改变代码结果 不,GetTickCount 确实返回处理器时钟周期数。 @HansPassant,老实说,您不会认为 SetPixel 对处理器的压力足以导致散热问题吗? 我相信 setpixel 实际上会写入屏幕外缓冲区,然后将其 bitblt 到实际屏幕(尽管这可能是大约 20 年前 Windows 使用的工作方式)。桌面管理器是否有可能检测到更改并仅复制更改的矩形,该矩形最终会变得更大,因此需要更长的时间? 【参考方案1】:

首先,清理一下您的测试代码:

#include<windows.h>

// number of pixels written in each run
#define NUM_PIXELS 50000

// range of pixel coordinates
#define MIN_RANGE 100
#define MAX_RANGE 1000
#define RANGE_MULT 10

// pause after each run to allow DWM to do its things
#define DWM_PAUSE 20 // seconds

HDC DC;

void bench(int range, int pause)

    int i, start;

    // let DWM digest previous pixels
    Sleep(pause*1000);

    // feed more pixels into the system
    start = GetTickCount();
    for (i = 0; i != NUM_PIXELS; i++)
    
        SetPixel(DC, rand()%range, rand()%range, 0);
    
    printf ("pause %d range %d duration %d\n", pause, range, GetTickCount()-start);


int main (void)

    DC=GetDC(GetDesktopWindow());

    int range;
    for (range = MIN_RANGE; range <= MAX_RANGE; range *= RANGE_MULT) bench(range, 0);
    for (range = MAX_RANGE; range >= MIN_RANGE; range /= RANGE_MULT) bench(range, 0);
    for (range = MIN_RANGE; range <= MAX_RANGE; range *= RANGE_MULT) bench(range, DWM_PAUSE);
    for (range = MAX_RANGE; range >= MIN_RANGE; range /= RANGE_MULT) bench(range, DWM_PAUSE);
    return 0;

在启用 Aero 桌面的 Win7 上运行此程序会产生以下结果:

c:\Dev\php\_***\C++\SlowSetPixel\Release>SlowSetPixel.exe
pause 0 range 100 duration 1404
pause 0 range 1000 duration 5273
pause 0 range 1000 duration 8377
pause 0 range 100 duration 3713
pause 20 range 100 duration 3089
pause 20 range 1000 duration 6942
pause 20 range 1000 duration 8455
pause 20 range 100 duration 3151

在禁用 Aero 的情况下运行相同的程序:

c:\Dev\PHP\_***\C++\SlowSetPixel\Release>SlowSetPixel.exe
pause 0 range 100 duration 47
pause 0 range 1000 duration 31
pause 0 range 1000 duration 31
pause 0 range 100 duration 31
pause 20 range 100 duration 63
pause 20 range 1000 duration 47
pause 20 range 1000 duration 47
pause 20 range 100 duration 62

有人偷了我的 CPU 吗?

是的,先生,我抓住了肇事者。

这些测试最好在打开任务管理器的情况下使用,以观察可怕的dwm.exe 桌面窗口(无能)管理器的工作情况。

在第一次执行期间,dwm.exe 被困在 100% CPU(使用我 PC 的 4 个内核中的一个),并且它的内存消耗上升到可笑的数量(从大约 28 Mb 增加到 112 Mb)。

即使有 20 秒的停顿,该死的 DWM 甚至还没有完成对像素的消化。这就是为什么测试的第二部分显示执行时间稍长的原因。

没有 Aero,SetPixel 函数基本上什么都不做。 DC 不是无效的,但是SetPixel 没有进行任何(可见的)修改。

什么鬼?

这一切发生的可能原因是,使用华丽的“新”(自 Vista 以来)桌面界面,最终桌面位图的合成是由 dwm.exe 进程完成的。每个窗口都有自己的图形缓冲区,dwm.exe 会收到任何更改的通知并重新计算背景中每个像素的最终外观。

将像素直接写入桌面窗口基本上会搞砸这个小计划,因为外部程序会访问据称是 dwm.exe 的私人游乐场。

我不知道微软的人是如何处理这个案子的,但显然他们没有以任何有效的方式来处理。 看起来桌面的多个副本已加载到内存中,可能是为了允许修改一个,而其他的则集成到合成链中。

dwm.exe 占用的内存量大约是 1000*1000 RGBA 位图大小的 25 倍。 测试表明,这个量随改性区域的表面而变化。

我怀疑这个愚蠢的过程每秒对屏幕进行 20 或 30 次采样,并在它发现发生了奇怪的事情(例如 SetPixel 调用)时创建屏幕修改部分的新副本。

有了这么糟糕的结果,我想知道他们为什么一开始就允许访问桌面 DC,除了允许人们用 10 行代码涂抹屏幕。

现在,高效的屏幕访问需要使用 DirectX 来绕过糟糕的向后兼容层,您必须通过过时的 Win32 GDI 来操作位图。

【讨论】:

【参考方案2】:

好吧,Kuroi neko 试图在某种程度上解释它,但我认为确切的原因没有得到回答。

为什么它会随着时间变慢?

==> 因为最终证明它是有序的nlogn

如何==>

    让我们从setpixel 的工作原理开始(可能会起作用,但我会说它是如何工作的)。首先也是最重要的任务是参数验证。谷歌参数验证(或堆栈交换)的成本。它还包括将屏幕数据映射到设备上下文,然后针对它测试传递的坐标。简而言之,这需要时间,这是应该考虑的。 像素有一些属性;所以它必须填写一些结构,然后必须创建调色板,然后填写颜色位 acc。到结构,然后它需要将此调色板映射到设备上下文。接下来,它需要清理这个像素的烂摊子,并且几乎类似(可能会少一点)的工作正在商店中用于下一个像素 现在是真正的肉:SetPixel() 是 gdi32.dll 中的函数,而 BOOM,图形子系统驻留在内核模式!因此,对于每个像素,都有Usermode -&gt; KernelMode -&gt; Usermode 的开销,这就是为什么第 1 点和第 2 点比通常情况更重要的原因。现在很容易猜到为什么它会随着时间的推移变慢。

MS 有没有尝试解决这个问题?

    是的,是的,实际上这是由于解决了其他问题而导致的问题。简单地说,图形子系统在最初的设计中并不是内核模式的一部分,但是提高性能他们将它移到了内核模式。 他们尝试了GetEnhMetaFile,这肯定是在SetPixel() 内部调用的

更重要的是 --> 你应该知道什么时候使用SetPixel()。如果您将它用于可以使用其他耗时较少的机制来完成的事情,那是您的错误,而不是 MS。您应该将其用于笔迹分析或类似用途,其中单个或非常少的像素值会产生影响

您绝对可以通过连续切换模式大约 100000 次来谷歌这意味着什么。

【讨论】:

以上是关于SetPixel 会随着时间的推移而变慢的主要内容,如果未能解决你的问题,请参考以下文章

当访问遍历记录集时,它是不是会随着索引的增加而变慢,为啥?

手势反应随着使用而变慢

AVCaptureVideoPreviewLayer随着时间的推移运行速度变慢

当计算相同时,为啥 R 会随着时间的推移而减慢?

如何正确计时用 Java Spring MVC 编写的 API?

Python For 循环随时间变慢