QBasic代码的优化【完全版】【译】

      编程宝典 2005-5-30 10:54:00

www.blogcn.com/User9/cpperl/blog/3303328.html
说明:本教程是在VirtuaSoft网页上获的(http://www.chainmailsales.com/virtuasoft)。由Cpperl@hotmail.com粗略翻译为中文,水平有限,错误在所难免,如果大家阅读时候发现错误请来信指出!

Optimizes QBasic Programming Guide
Danny Gump 撰写

本文是为使用QB的程序设计者编写的指南,目的在于帮助他们优化程序代码获得更高的效率。如果你有任何问题,请通过gump@gnt.net或者danielgump@chainmailsales.com联系Danny。另外请注意:这不是为初学者编写的教程。因为需要深厚的QB程序编写的背景。

第一部分 数学计算

a. 因式分解
b. 整数的使用
c. \ vs./
d. 预先设定的数据表

a. 因式分解

如果可能的话,多步的数学计算应该被完全分解合并同类项以至于计算机只需花费最少的步骤来计算出最后的结果。我曾经用QB编写了一个纹理映射库,在把一个象素输出到屏幕之前需要43步来计算结果。这导致了该程序运行得非常的缓慢,但是对全部得整式分解合并同类项之后,我把步骤减少到24步,因此该程序速度大概提高了两倍,尽管对实时实现的多边形来说它仍然很慢!例子:

之前:
以下内容为程序代码:

POKE perx2*x1&*pery2\dx\dy+perx2*x4&*pery\dx\dy+perx*...
x2&*pery2\dx\dy+perx*x3&*pery\dx\dy+320*pery2*...
y1&*perx2\dx\dy+320*pery2*y2&*perx\dx\dy+320*...
pery*y4&*perx2\dx\dy+320*pery*y3&*perx\dx\dy, color

之后:
以下内容为程序代码:

POKE ((x1&*pery2+x4&*pery)*perx2+(x2&*pery2+x3&*pery)*...
...perx)\dx\dy+320*(((y1&*perx2+y2&*perx)*pery2+...
...(y4&*perx2+y3&*perx)*pery)\dx\dy), color

不用担心你需要理解上面的等式;它们仅仅用来展示因式分解合并同类项项后长度的减少。这样你可以看见因式分解合并同类项后式子是何等的简洁,不仅仅计算时间减少了,代码也变短了。这对一个程序设计者来说是一箭双雕的结果。我同时也发现了通过因式分解一组计算,得到的数据更加的精确,数据也更加的平滑。所以看来应该是三个好处。

b. 整数的使用

QB有四种变量类型:整型,长整型,单精度小数,双精度小数。自电脑被设计使用二进制整数计算数字以来,似乎仅仅在BASIC中整数对于电脑来说比浮点数更易于使用。小数不是直接的保存在内存中,而且要花去许多步骤来加载一个小数到内存。当需要小数会进行大量的计算,这些步骤能够显著的延缓程序的运行速度。因为内存中二进制是由整数表示(2的非负幂),小数实际作为分数部分储存(2的负幂)。而计算这个小数,要消耗大量时间因为这些值不是真正存在于内存而是通过BASIC计算生成。尽可能的使用整数将消除进行这些不必要计算的时间,所以增快了程序运行的速度。

c. \ vs./

每个使用BASIC的程序设计者都知道运算符/表示什么,但是除非你写过很长一段时间程序,不然你有可能用不上这个运算符\。运算符\用来整除(和运算符/相反,运算符/表示浮点除法)。就像上面文章提到的那样,尽可能的使用整数,整数也是符合那种原则。

但是为什么要在用2除8这样的算式中使用运算符\呢?当然,它们都是整数,这一点我们都知道。但是没有关系因为我们不是在程序运行时候唯一处理这个的。8/2对电脑来说它并不认为是整数的除法,它认为是8.0000000/2.0000000。它会把整数转换为浮点数来进行除法,然后把结果又转回整数。这样怎么会有效率呢?

我们来看另外一个例子:9除以4。数学课上这会是9/4=2.25。但是在计算机运算中将是9.0000000/4.0000000=2.2500000或者9\4=2。现在你明白了,整除是不会产生小数结果的,而且它自动截取或者将答案取整。所以当使用它的时候要小心,因为有时候你想要最精确的结果而有时候却要极大程度的提高运行数度。这是你的选择。

d. 预先设定数据表(Premade Tables)

有时候你可能想使用一些BASIC的内置函比如SIN或者COS来输出小数答案。但是为什么要使用小数当你能使用整数来取代那些缓慢浮点计算的时候?这个时候应该用一张取特定整数值的表来解决问题。但这会降低精度吗?不会的,它涉及任何显而易见一些角度。到现在为止你大概想知道当需要小数的时候如何使用整数来进行计算吧.这种情况用于让你的表有100倍,1000倍或者10000倍的函数为某种输入正常返回值。之后由这些100,1000,10000简单的整除得到最后的计算结果。例子:

以下内容为程序代码:

table:
cosine(0)=100:cosine(10)=98
cosine(20)=94:cosine(30)=87
cosine(40)=77:cosine(50)=64
cosine(60)=50:cosine(70)=34
cosine(80)=17:cosine(90)=0

start:
? 500*cosine(0) \100 500
? 500*COS(0) 500
? 500*sine(45) \100 353
? 500*SIN(45) 353.5533906

瞧,你看到了。在整型值和小数值之间没有太大的背离,所以这可以被用来为任何数学运算来输出整数,比如旋转图形的时候。

第二部分 直接存取(POKE和PEEK)

a. 决不要使用PSET和POINT
b. 整数 vs. 长整数
c. 内存块操作(Memory Segments)

a.决不要使用 PSET

QBasic中PSET语句非常缓慢。因为它太一般化以至于它能在每种图形模式下工作。而POKE就比较特殊,这是显然的。简单的给出内存块中地址,可以直接超作内存地址的值。POINT和PSET差不多的缓慢。PEEK应该被用来在任何可能的地方代替POINT。

我将提示你在什么地方应该使用或者不使用POKE或者PEEK,这样你就不会像我刚意识到这一点的时候那样糊涂了。你应该坚持在13H以下的任何图形模式中使用PSET和POINT。我这样建议是因为这种情况下很难控制内存的分配和寻址。一些地方每一象素是4bits,另外的是2bits或者1bit。一些是通过屏幕交换页面(screen page)来分配交换内存,而其他地方直接使用64K以上的内存来进行内存分配和寻址。(如果你不知道你在做什么的话这是非常糟糕和凌乱的)而在使用13H的显示模式中字符或者字符色彩显示是每象素1byte,这样就很容易寻址。(内存寻址将在后面涉及)

b. 整数 vs. 长整数

如果你已经尝试过在使用13h(320*200*256)的显示模式下来填充(POKE)色彩直接写色彩值,你可能会注意到在屏幕的中下部,会在一个值大于32767的时候有整数变量将会溢出。你可能认为应该使用长整型来避免溢出,其实不是那样。对于使用过汇编的人来说,你会注意到16bit的整数没有负值,所以他们能够超出BASIC的32767直接使用到65535。实际上,这只是部分的正确。汇编的16-bit数是有负值的。例如,-5=-5+65535=65531(模运算)。汇编中的一个负数和该数加上相应类型数的2的多少次方的结果一样。这样就可以在13h下用来填充(POKE)直接写屏幕的底部了。例如要POKE 32768,简单的运算32768-65536=-32768,你可以这么写语句:POKE -32768,color就达到目的了。

c. 内存块的超作(Memory Segments)

下面是对编程很重要的内存分块地址和偏移量:

Segment Offset
Graphics &HA000 0
Text &HB800 0
ASCII characters &HFFA6 14

分块地址通过这种方式得到:DEF SEG=[Segment #]而偏移量使用:PEEK([Byte]+[Offset])

第三部分 PALETTE调色板色彩相关

a. PALETTE USING方法
b. 直接从硬件输出
c. 直接从硬件读入

a. PALETTE USING方法

PALETTE USING是QB的一条语句,是用来立刻写所有的256颜色到PALETTE中来代替每次写一个的。这样做就增加了速度。一般使用一个256个元素的长整型数组中放置色彩值,每个元素都有色彩的PALETTE的RGB值。(65536*red+256*green+blue 值是0-63)。

PALETTE USING 这样使用:

以下内容为程序代码:

DIM PaletteVariable(255) AS LONG
...
PALETTE USING PaletteVariable(0)
...

但是为什么当直接写硬件的时候速度有了大概60倍的提高?

b. 直接硬件输出

这是最根本的初始化PALETTE方法。它可以一眨眼就完全改变整个256色的PALETTE。它如此的快以至于很轻松的得到旋转PALETTE等效果。但是下面这一部分稍微有一些难学,但是一旦理解,就有巨大的价值。如下方法使用:

以下内容为程序代码:

OUT &H3C8, ColorNumber
OUT &H3C9, Redvalue
OUT &H3C9, Greenvalue
OUT &H3C9, Bluevalue

这里的COLOR值,和PALETTE USING中的一样,从0到63。当使用这个方法的时候, 像上面一样,很容易让一个有256个元素的长整型数组来用作PALETTE USING。同时,一些直接取地址值的方法应该被用来进一步的增加程序的速度和效率。下面是一个例子:

以下内容为程序代码:

DIM PalVar(255) AS LONG
...
DEF SEG=VARSEG(PalVar(0))
BLOAD "Palette.pal", VARPTR(PalVar(0))
FOR Temp%=0 to 255
OUT &H3C8, Temp%
OUT &H3C9, PEEK(VARPTR(PalVar(Temp%)))
OUT &H3C9, PEEK(VARPTR(PalVar(Temp%))+1)
OUT &H3C9, PEEK(VARPTR(PalVar(Temp%))+2)
NEXT

这个例子载入了一个独立的PALETTE文件,但是如果你在程序中定义了所有的色彩那就不需要了。如果你没有旋转PALETTE,这个方法仍然是不需要的;PALETTE USING方法可以做的很好。

c. 直接硬件读入

这和B的方式是相反的过程。通过这个超作,你可以直接从硬件读入PALETTE的值。使用方法如下:

以下内容为程序代码:

OUT &H3C7, ColorNumber
Redvalue = INP(&H3C9)
Greenvalue = INP(&H3C9)
Bluevalue = INP(&H3C9)




这个对当前屏幕色彩校正和屏幕淡化来说是非常有用的。

第四部分 DEBABELIZING

a. 真彩色 -> 8bit位色彩
b. 降低图像分辨率
c. 在程序中使用图像调色板
d. 配置调色板

a. 真彩色 -> 8bit位色彩

如果你想使用扫描的图象或者让程序有高质量的视觉表现,就必须首先用一些图形处理程序打开它们然后存为8位的图片。因为在QBasic中大部分是使用13H屏幕模式来处理色彩。它仅仅有256(8位)种色彩而大部分的扫描仪是24位(真彩)。只有这样你才能成功地用QBasic来打开图片而不会出错。

b. 降低图像分辨率

大多数绘图程序有重新调整图片的选项。为了使图片适合13H,你必须确定图片的分辨率是320*200或者比这个低。所以使用一些绘图程序重新调整解析度来配合你程序的需要。如果你想重新调整一个QBasic中已有的图片,载入屏幕然后使用下面这个公式:

以下内容为程序代码:

FOR x% = startx% TO endx%
FOR y% = starty% TO endy%
PSET (x%, y%), POINT(x% * Factorx%, y% * Factory%)
NEXT
NEXT

或者

以下内容为程序代码:

DEF SEG = &HA000
FOR x& = startx& TO endx&
FOR y& = starty& TO endy&
POKE x& + 320 * y&, PEEK(x& * Factorx& + 320 * (y&*Factory&))
NEXT
NEXT

说明:Factorx和Factory是图像被压缩的系数。

c. 在程序中使用图像调色板

所有的8位图片格式资料在有调色板的绘图程序都是公开的。我没有每个文件存储格式存储色彩的所有的细节(可以到www.wotsit.com寻找)但是我可以在使用BMP格式方面提供帮助。你可以试试我网页上的读入程序,地址在:www.gnt.net/~gump的“Files->Utilities”下面。这个读入程序中使用了一个长整型数组pal来存储图片的调色板信息。完全的存储了所有256个元素,从数组的array[0]到array[255]来作为一个调色板色彩文件当你重新载入的时候你就有你自己的调色板了。
c. 配置调色板

现在你可以在QBasic中载入图片了,但是每个图片有不同的色彩信息应该怎么办呢?如果尝试载入在屏幕上一次载入2个或者更多的图片,确实会扰乱色彩(除非是灰度图象,它们有一样的灰度色彩模版)。 所以你怎么在一个相同的调色板中放置更多的图片?如果要这么做的话,你必须使用AdobePhotoshop或者其他的允许配置调色板的图象处理程序来处理了。你让你的调色板所有的值扩展4倍来配置,因为所有图形文件格式保留它们调色板的值在0到255之间同时对non-SuperVGA的屏幕是在0-63之间。之后你参考本章第一部分,增加使用配置的调色产来改变色彩为256这一个步骤就可以了。

第五部分 子图形画面(SPRITES)

a. 什么是子图形画面?
b. 实现子图形的子程序编写

a. 什么是子图形画面?

子图形画面就是程序中的一些既不是前景,也不是背景的图像。一般来说,移动的图像就是子画面。在游戏超级玛莉和超人(MegaMan)中可以看见这些例子。

b. 实现子图形的子程序编写(在屏幕模式 13H中)

在你进一步阅读之前,为了使你的进一步学习更有效,请你确定你已经阅读过有关POKE、PEEK和INTEGER使用的相关的教程。自从不再使用Commodore128和它的内建的字图形程序,我们就需要通过自己设计来解决这一问题。QBasic的GET和PUT既不能在图像周围保留黑色区域(Black Box)(PEST)也不能显示黑色只能让图形失真(XOR),所以那样是没有办法的。要绘制透明背景的子图像而不是把画面涂黑,就需要一像素一像素的绘制,同时在绘制之前校验是否是黑的。最简单的方法就是将每个单元按一个整数以GET-PUT的方式来保存该图像,例如,DIM picture(32001) as INTEGER。下面是整数GET-PUT数组的格式:

array%(0) =按位bit表示的图像宽度
array%(1) = 以像素pixel表示的图像高度
array%(2)
... = 一系列像素值的列表(每个单元两个值)
array%(n)

为了得到图像的像素宽度,应该把每个数组单元以8为单位加以分开,例如width%=array%(0)\8。而图像的高度等于数组元素1。开始着手绘制子图像的时候,你必须定义数组的内存块区,例如DEF SEG=VARSEG(ARRAY%(0))。这是因为你将要从数组直接向内存地址写值。在你的调色板里,黑色的值是color # 0,所以这个实现子图像的子程序在碰到它的时候应不绘制任何像素。以上基本的准备已经讲完了,只需要自己具体的子图像实现程序和所有需要的值,就可以制作你自己程序中的子图像了。下面你看到的是一个完成得的子图像的实例,它已经包含在VirtuaSoft的图形库之中。

以下内容为程序代码:

SUB sprite (x%, y%, file$)
DIM picture(32001) AS INTEGER
DEF SEG = VARSEG(picture(0))
BLOAD file$, VARPTR(picture(0))
FOR tempx% = 0 TO picture(0) \ 8 - 1
FOR tempy% = 0 TO picture(1) - 1
temp% = PEEK(VARPTR(picture(2)) + tempx% + (picture(0) \ 8&) * tempy%)
IF temp% > 0 AND tempx% + x% >= 0 AND tempx% + x% < 320 AND tempy% + y% >= 0 AND tempy% + y% < 200 THEN
PSET (x% + tempx%, y% + tempy%), temp%
END IF
NEXT
NEXT
DEF SEG = &HA000
END SUB

说明:x%和y% 是传给子程序的值用来表示子图像左上角的坐标位置,file$用来存储数组。

如果你将要使用这些代码在你的程序中,希望你能在荣誉出品(Credits)一列中提到VirtuaSoft或者通过电子邮件寄到gump@gnt.net给我们一份你作品的拷贝这样我们将因为自己的教程或者代码给大家带来的帮助和便利倍感荣耀。

第六部分 双缓冲

a. GET-PUT 存储方式
b. 使用GET-PUT进行双缓冲操作

a. GET-PUT 存储方式

GET和PUT是现存对图像存储最有效的方法。这种方式基本上知道图像的宽和高而且列出了图像中所有色彩的值。下面是在屏幕模式13H中以GET-PUT方式存储图像为一个整型数组的分解解说。

array%(0) = 用位bit表示的图象宽
array%(1) = 用像素表示的图像高度
array%(2)
... = 像素值列表
array%(n)

为了使用双缓冲技术,数组的0元素将被赋值2560,因为13H以每8位表示一个像素,一共有320个像素,那么320x8=2560。数组的1元素被赋值200像素用来表示高。然后所有剩下的元素是每个元素有两像素。(因为整数是16位而一个像素是8位),所以数组看上去就是下面这个样子:

array%(0) = 2560
array%(1) = 200
array%(2) = pixel at 0,0, pixel at 1,0
...
array%(n) = pixel at 318,199, pixel at 319,199

b. 使用GET-PUT来进行双缓冲操作

既然你是在13H模式下使用GET-PUT数组的高手,你应该懂得足够多来直接从内存向数组存放数据。它基本上和向屏幕写数据差不多,除了内存块地址不同之外。下面是向数组存放数据与向屏幕写数据之间的比较的异同:

以下内容为程序代码:


SCREEN:
DEF SEG = &HA000
POKE x% + 320& * y%, color%

GET-PUT ARRAY:
DEF SEG = VARSEG(array%(0))
POKE VARPTR(array%(2)) + x% + 320& * y%, color%

没有太大的差别吧,仅有的是你定义不同的内存地址同时需要考虑数组的偏移(13H的屏幕模式中偏移是0,所以之前没有使用什么偏移)。用这种方法来双缓冲,完全的直接通过GET-PUT数组来在你的屏幕上绘制图像,这样在屏幕上输出最终图像:PUT(0,0),array%,PSET。如果你想知道如何通过GET-PUT方式直接向数组写入线和圆这些,我将开始制作一个包含这些功能的库将最终在我的Web站点发布。

第七部分 BSAVE和BLOAD

BLOAD和BSAVE是直接操作内存地址,而不像OPEN,所以你需要定义在内存中开始的地址以便于文件知道从什么地方开始读写。对于BSAVE来说,同时需要告诉它开辟的文件具体有多大多少字节。BSAVE和BLOAD的使用语法如下:

以下内容为程序代码:


DEF SEG = Segment
BSAVE "FileName", Offset, NumBytes

DEF SEG = Segment
BLOAD "FileName", Offset

说明:NumBytes从0开始计数,就是比正式的数值少1。

要从图像图形屏幕模式直接存储图像,要使用内存块区&HA000和地址偏移量0;而在文字图形模式使用内存块区&HB800和0;对于数组来说使用VARSEG(array%(0))和VARPTR(array%(0))。BLOAD和BSAVE能快速的存取文件但是也因为这一点存在问题。因为你必须在你使用它们之前对你在做什么很清楚。下面是一些常见的因为错误的使用这种快速的方法而导致电脑的崩溃的原因。

1.某个文件被存入一个太小的数组。BLOAD不知道内存中是什么。它继续写直到完成为止。请确定所有的相关的数组能够容纳下该文件的大小。

2.该文件没有被适当的读入数组。你必须确定你在BLOAD中使用了正确的偏移。如果数组有不止一个元素(array[0]),你需要使用VARPTR(array(0))。同时,DEF SEG必须被设置指向第一个元素(array[0])。如果数组是多维的,确定BLOAD考虑了这个问题。如果这个数组没有定义元素,就不需要在BLOAD中做比VARPTR(array)更多的其他事情。

3.文件没有正确的存储。就像上面解释的那样,确定你为数组分配了适当的内存块区地址和偏移量

(完)
标签集:TAGS:
回复Comments() 点击Count()
喜欢就顶一下

回复Comments

{commentauthor}
{commentauthor}
{commenttime}
{commentnum}
{commentcontent}
作者:
{commentrecontent}