browser icon
You are using an insecure version of your web browser. Please update your browser!
Using an outdated browser makes your computer unsafe. For a safer, faster, more enjoyable user experience, please update your browser today or try a newer browser.

键歇(2)

Posted by on 2014 年 03 月 04 日

你可以任意转载本文,但请在转载后的文章中注明作者和原始链接。
媒体约稿请联系 titilima_AT_163.com(把“_AT_”换成“@”)。

上一段代码给列位品品,先:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void WINAPI RectangleSlow(
    __in HDC hdc,
    __in int left, __in int top,
    __in int right, __in int bottom,
    __in COLORREF border, __in COLORREF inner)
{
    int i, j;
 
    // 绘制横向边框
    for (i = left; i < right; ++i) {
        SetPixel(hdc, i, top, border);
        SetPixel(hdc, i, bottom - 1, border);
    }
 
    // 绘制纵向边框
    for (i = top + 1; i < bottom; ++i) {
        SetPixel(hdc, left, i, border);
        SetPixel(hdc, right - 1, i, border);
    }
 
    // 填充矩形内部
    for (j = top + 1; j < bottom - 1; ++j) {
        for (i = left + 1; i < right - 1; ++i) {
            SetPixel(hdc, i, j, inner);
        }
    }
}

估计有的少年会忍不住吐槽了:有你这么玩的吗,Windows 难道就没有可以直接画矩形的 API 吗!

是的,我当然知道可以用 Rectangle 函数直接绘制一个矩形;但是,这个函数真正的硬伤并不是我重新造轮子这件事,而是效率——如你所见,我给这个函数起名叫 RectangleSlow 已经说明一切了。

由于画矩形的这个问题只是本篇《键歇》的一个话题引子,所以我在这里也就不再卖关子让诸君来猜测其中的瓶颈何在了,而是直接用下面的表格来说明 RectangleSlow 和 Rectangle API 的效率差距:

RectangleSlow Rectangle
特权切换 每次 SetPixel 都会从用户态进入到内核态,调用 win32k.sys 的 NtGdiSetPixel。 调用 win32k.sys 的 NtGdiRectangle,只切换一次特权。
每次 SetPixel 都会对 HDC 加锁、解锁一次。 对 HDC 加锁、解锁一次。

举例而言,如果要绘制一个 100×100 的矩形,那么 RectangleSlow 会比 Rectangle 多上 9998 次特权级别切换的操作,以及至少 9998 次加锁和解锁的操作。至于这个差距到底有多少,蛋疼的有兴趣的骚年们可以打个 GetTickCount 实测一下。

下面开始正文话题。

引申话题一:轮子与效率

诚然,画矩形只是一个例子。在现实的环境中,我们所遇到的问题肯定会更加复杂,比如各种非主流的扭曲、奇葩的阿尔法混合和匪夷所思的渲染等等——总之,我所描述的是一个所有现成的图形库都不敷其用的场景,你唯一能做的就是必须自己来造这个轮子,此场景还请自行脑补之。

有道是大道至简万物归宗,无论多么复杂的绘制,其实质内容大抵可以概括为一系列逐点运算后的像素填充操作,无非是算如何算、填如何填,仅此而已——换句话说,用数学计算和 GetPixel/SetPixel 就能搞定所有的问题。但是,如果一次操作所涉及到的像素点达到了上万个点甚至更多的话,那么我们势必会面对和 RectangleSlow 同样的效率瓶颈,这怎么破?

答曰:见招拆招,佛挡杀佛,墙挡踢墙。针对之前表格中所列的两条,我们可以很容易地得到两点优化方案,如下:

  • 减少从用户态到内核态特权切换的次数。
  • 在保证绘制操作不跨线程的前提下,去掉画点时加锁和解锁的操作。

说白了,就是获取目标 HDC 的原始 DIB 缓冲区,然后直接对这段 DIB 进行最直接的内存读写操作;另外,你可以用 Win32 API CreateDIBSection 去拿这个 DIB,不谢。

引申话题二:跨栈

如果一定要为 RectangleSlow 的出现找一个理由,那么我觉得最可能的解释应该是这样的:一个刚刚接触 Win32 的程序员,他有一个很急的任务——画一个矩形,但是他没有时间去在文档中找画矩形的函数(甚或他根本就不知道会有这么个函数);所幸,他知道 Win32 下画点的 API,便自己撸了个 RectangleSlow 函数出来。

也许你会觉得我编的这个故事有些图样图森破了,那么以下的场景你怎么看呢元芳?

  • 你在编写一个 SQL 数据库客户端。由于时间紧迫,你根本没有时间去学习嵌套 SELECT 的写法,也可能根本不知道有索引、触发器和存储过程这些东西的存在,所以你不得不用成倍篇幅而且效率低下的 C/C++ 代码来实现所需的功能……
  • 你在你的客户端内嵌入了 Lua 解释器。由于时间紧迫,你根本没时间去了解 string 库的 match/gsub(甚或根本不知道它们的存在),但是你又需要有一个字符串匹配/替换的功能,所以你不得不在 C/C++ 端使用正则表达式来完成这个操作。
  • 为了发布软件,你需要写一个网站来展示。由于时间紧迫,你根本没时间去学习 CSS 和 jQuery 等技术,所以你不得不用 <table> 标签和最原始的 JS 接口去撸一个扩展性极差的站点。
  • ……

此类的例子举不胜举,但其实都大同小异:在某种不可抗力的驱使下,你不得不跨栈堆码,从而远离自己的舒适区;又因为时间所限,你只有很少的时间来熟悉跨栈后的新环境,于是只得把某些难点使用自己所熟悉的技术(语言)来实现,虽然这样写出的代码很可能是繁冗且低效的。

再回到 RectangleSlow 所画的那个 100×100 的矩形上。这个函数在我一台 2005 年购置的老插屁笔记本电脑上的计时成绩是 20 毫秒,与之相比,Rectangle API 的计时则小于 1 毫秒(这个调用已经超越了 GetTickCount 所能达到的最小精度)。 虽然从数字上来看二者效率的差距让人发指,但其实人类的肉眼是无法分辨 20 毫秒和 1 毫秒的差别的。因此在我看来,千钧一发之际,只要最终的结果可以接受,那么选择自己熟悉的东西来实现需求是无可厚非的,毕竟条条大路通罗马。

不过,在完活之后你有没有兴趣真正地跨栈过去,看看你并不熟悉的另一种精彩是什么样的呢?——至少这样可以使得你下一次再跨栈的时候不至于再那么陌生、局促与紧张,不是吗?

哦,我得说一句,这一段《跨栈》不是鸡汤,下一个话题才是。

引申话题三:全栈

记得今年年初的时候我看到了个名称,叫“全栈工程师”(当然,也有叫“全栈程序员”的)。我十分不以为然,因为这个“全”字虽然讨喜,然而硬伤最大的也恰恰是这个字——我们那蝴蝶翩翩飞的庄周先生一句话就可以秒杀它:

吾生也有涯,而知也无涯。以有涯随无涯,殆已!

可能有的少年会驳斥我对“全”字的定义太过绝对,因为这实在是有偷换概念之嫌。那么,我且不再纠结于这个字的意义,而是继续以 RectangleSlow 为例来说明我的态度吧:

  • 对于临栈已久、只知奋力撸 RectangleSlow 而不知有 Rectangle 者,又何须在意此人是否“全”呢?
  • 对于初临栈写了 RectangleSlow 而坚信会有 Rectangle 并后学之者,又何须在意此人是否“全”呢?
  • 对于初临栈便猜测会有 Rectangle 而速学之并用之者,又何须在意此人是否“全”呢?

以上。

订阅本站

2 Comments

  • At 2014.07.03 15:55, 匿名 said:

    cool

    • At 2014.09.21 10:10, pc said:

      善!

      (Required)
      (Required, will not be published)