home *** CD-ROM | disk | FTP | other *** search
/ Collection of Hack-Phreak Scene Programs / cleanhpvac.zip / cleanhpvac / WINER.ZIP / CHAP9.TXT < prev    next >
Text File  |  1994-09-01  |  47KB  |  1,058 lines

  1.                                 CHAPTER 9
  2.  
  3.                           PROGRAM OPTIMIZATION
  4.  
  5. Throughout the preceding chapters I have shown a variety of tips and
  6. techniques that can help to improve the efficiency of your programs.  For
  7. example, Chapter 6 explained that processing files in large pieces reduces
  8. the time needed to save and load data.  Likewise, Chapter 8 discussed the
  9. improvement that SWAP often provides over conventional assignments.  Some
  10. optimizations, however, do not fit into any of the well-defined categories
  11. that have been used to organize this book.  In this chapter I will share
  12. several general optimization techniques you can employ to reduce the size
  13. of your programs and make them run faster.
  14.    The material in this chapter is organized into three principle
  15. categories: programming shortcuts and speed improvements, miscellaneous
  16. tips and techniques, and benchmarking.  Each section addresses BASIC
  17. programming ideas and methods that are not immediately obvious in most
  18. cases.
  19.  
  20.  
  21. PROGRAMMING SHORTCUTS AND SPEED IMPROVEMENTS
  22. ============================================
  23.  
  24. Chapter 3 discussed the use of AND, OR, and the other logical operations
  25. that can be used for both logical (IF and CASE) tests and also bit
  26. operations.  But there are a few other related points that are worth
  27. mentioning here.  When you need to know if a variable is zero or not, you
  28. can omit an explicit test for zero like this:
  29.  
  30.    IF Variable THEN...
  31.  
  32. You might be tempted to think that two variables could be tested for non-
  33. zero values at one time in the same way, using code such as this:
  34.  
  35.    IF Var1 AND Var2 THEN...
  36.  
  37. However, that will very likely fail.  The expression Var1 AND Var2 combines
  38. the bits in these variables, which could result in a value of zero even
  39. when both variables are non-zero.  As an example, if Var1 currently holds
  40. a value of 1, its bits will be set as follows:
  41.  
  42.    0000 0000 0000 0001
  43.  
  44. Now, if Var2 is assigned the value 2, its bits will be set like this:
  45.  
  46.    0000 0000 0000 0010
  47.  
  48. Since no two bits are set in the same position in each variable, the result
  49. of Var1 AND Var2 is zero.  An effective solution is IF Var1 * Var2 THEN to
  50. ensure that neither variable is zero.  And to test if either variable is
  51. non-zero you'd use OR.  Whatever follows the test IF Var1 OR Var2 THEN will
  52. be executed as long as one (or both) variables are not zero.  These are
  53. important short cuts to understand, because the improvement in code size
  54. and execution speed can be significant.
  55.    Each of the AND, OR, and multiplication tests shown here generates only
  56. 11 bytes of code.  Contrast that to the 28 bytes that BC creates for the
  57. alternative: IF Var1 <> 0 AND Var2 <> 0 THEN.  Because of the improved
  58. method of expression evaluation in BASIC PDS, this last example generates
  59. only 14 bytes when using that version of BC.  None the less, if you can
  60. avoid explicit comparisons to zero you will go a long way toward improving
  61. the efficiency of your code.
  62.    This short cut is equally appropriate with LOOP comparisons as well as
  63. IF tests.  In the BufIn function shown in Chapter 6, INSTR was used to see
  64. if a CHR$(13) carriage return was present in the buffer.  In the statement
  65. CR = INSTR(BufPos, Buffer$, CR$), CR receives either 0 if that character
  66. is present, or a non-zero position in the string where it was found.  The
  67. LOOP statement that surrounded the buffer searching uses LOOP WHILE CR,
  68. which continues looping as long as CR is not zero.
  69.    When an integer variable is compared in a LOOP WHILE condition, seven
  70. bytes of code are generated whether it is compared to zero or not.  But
  71. when a long integer is used to control the LOOP WHILE condition, omitting
  72. the explicit test for zero results in 11 bytes of compiled code where
  73. including it creates 20 bytes.  Note that with floating point values
  74. identical code is generated in either case, because an explicit comparison
  75. to zero is required and added by the compiler.
  76.  
  77.  
  78. PREDEFINING VARIABLES
  79.  
  80. Another important point is illustrated in the same code fragment that uses
  81. INSTR to search for a carriage return.  There, the CR$ string variable had
  82. been assigned earlier to CHR$(13).  Although the BufIn code could have used
  83. CR = INSTR(BufPos, Buffer$, CHR$(13)) instead of a previously defined
  84. string variable to replace the CHR$(13), that would take longer each time
  85. the statement is executed.  Since CHR$ is a function, it must be called
  86. each time it is used.  If CR$ is defined once ahead of time, only its
  87. address needs to be passed to INSTR.  This can be done with four bytes of
  88. assembly language code.
  89.    If CHR$(13) will be used only once in a program, then the only savings
  90. afforded by predefining it will be execution speed.  But when it is needed
  91. two or more times, several bytes can be saved at each occurrence by using
  92. a replacement string variable.  Other common CHR$ values that are used in
  93. BASIC programs are the CHR$(34) quote character, and CHR$(0) which is often
  94. used when accessing DOS services with CALL Interrupt.
  95.    Likewise, you should avoid calling any functions more than is absolutely
  96. necessary.  I have seen many programmers use code similar to the following,
  97. to see if a drive letter has been given as part of a file name.
  98.  
  99.    IF INSTR(Path$, ":") THEN 
  100.      Drive$ = LEFT$(INSTR(Path$, ":") - 1)
  101.    END IF
  102.  
  103. A much better approach is to invoke INSTR only once, and save the results
  104. for subsequent testing:
  105.  
  106.    Found% = INSTR(Path$, ":")   'save the result from INSTR
  107.    IF Found% THEN
  108.      Drive$ = LEFT$(Path$, Found%) - 1)
  109.    END IF
  110.  
  111. The same situation holds true for UCASE$, MID$, and all of the other BASIC
  112. functions.  Rather than this:
  113.  
  114.    IF INSTR(UCASE$(MID$(Work$, 3, 22)), "/A") THEN A = True
  115.    IF INSTR(UCASE$(MID$(Work$, 3, 22)), "/B") THEN B = True
  116.    IF INSTR(UCASE$(MID$(Work$, 3, 22)), "/C") THEN C = True
  117.  
  118. Instead use this:
  119.  
  120.    Temp$ = UCASE$(MID$(Work$, 3, 22))
  121.    IF INSTR(Temp$, "/A") THEN A = True
  122.    IF INSTR(Temp$, "/B") THEN B = True
  123.    IF INSTR(Temp$, "/C") THEN C = True
  124.  
  125. Where the first example generates 138 bytes of code, the second uses only
  126. 111.  The time savings will be even more significant, because BASIC's
  127. UCASE$ and MID$ functions allocate and deallocate memory by making further
  128. calls to BASIC's string memory management routines.
  129.    Indeed, it is always best to avoid creating new strings whenever
  130. possible, precisely because of the overhead needed to assign and erase
  131. string data.  Each time a string is assigned, memory must be found to hold
  132. it; add to that the additional code needed to release the older, abandoned
  133. version of the string.
  134.    This has further ramifications with simple string tests as well.  As
  135. Chapter 3 explained, testing for single characters or the first character
  136. in a string is always faster if you isolate the ASCII value of the
  137. character first, and then use integer comparisons later.  In the example
  138. below, the first series of IF tests generates 60 bytes of code.  This is
  139. much less efficient than the second which generates only 46, even though
  140. the steps to obtain and assign the ASCII value of Answer$ comprise 12 of
  141. those bytes.
  142.  
  143. PRINT "Abort, Retry, or Fail? (A/R/F) ";
  144. DO
  145.   Answer$ = UCASE$(INKEY$)
  146. LOOP UNTIL LEN(Answer$)
  147.  
  148. '----- Method 1:
  149. IF Answer$ = "A" THEN
  150.   REM
  151. ELSEIF Answer$ = "R" THEN
  152.   REM
  153. ELSEIF Answer$ = "F" THEN
  154.   REM
  155. END IF
  156.  
  157.  
  158. '----- Method 2:
  159. A% = ASC(Answer$)
  160. IF A% = 65 THEN
  161.   REM
  162. ELSEIF A% = 82 THEN
  163.   REM
  164. ELSEIF A% = 70 THEN
  165.   REM
  166. END IF
  167.  
  168. Another prime candidate for speed enhancement is when you need to create
  169. a string from individual characters.  The first example below reads the 80
  170. characters in the top row of display memory, and builds a new string from
  171. those characters.
  172.  
  173.    Scrn$ = ""
  174.    FOR X = 1 TO 80
  175.      Scrn$ = Scrn$ + CHR$(SCREEN(1, X))
  176.    NEXT
  177.  
  178. Since we already know that 80 characters are to be read, a much better
  179. method is to preassign the destination string, and insert the characters
  180. using the statement form of MID$, thus:
  181.  
  182.    Scrn$ = SPACE$(80)
  183.    FOR X% = 1 TO 80
  184.      MID$(Scrn$, X%, 1) = CHR$(SCREEN(1, X%))
  185.    NEXT
  186.  
  187. An informal timing test that executed these code fragments 100 times using
  188. QuickBASIC 4.5 showed that the second example is nearly twice as fast as
  189. the first.  Moreover, since BASIC's SCREEN function is notoriously slow,
  190. the actual difference between building a new string and inserting
  191. characters into an existing string is no doubt much greater.
  192.  
  193.  
  194. INTEGER AND LONG INTEGER ASSIGNMENTS
  195.  
  196. Another facet of compiled BASIC that is probably not immediately obvious
  197. is the way that integer and long integer assignments are handled by the
  198. compiler.  When many variables are to be assigned the same value--perhaps
  199. cleared to zero--it is often more efficient to assign one of them from that
  200. value, and then assign the rest from the first.  To appreciate why this is
  201. so requires an understanding of how BASIC compiles such assignments.
  202.    Normally, assigning an integer or long integer variable from a numeric
  203. constant requires the same amount of code as assigning from another
  204. variable.  The BASIC statement X% = 1234 is compiled to the following 6-
  205. byte assembly language statement.
  206.  
  207.    C7063600D204  MOV WORD PTR [X%],1234
  208.  
  209. Assigning the long integer variable Y& requires two such 6-byte
  210. instructions--one for the low word and another for the high word:
  211.  
  212.    C7063600D204  MOV  WORD PTR [Y&],1234 ;assign the low word
  213.    C70638000000  MOV  WORD PTR [Y&+2],0  ;then the high word
  214.  
  215. The 80x86 family of microprocessors does not have direct instructions for
  216. moving the contents of one memory location to another.  Therefore, the
  217. statement X% = Y% is compiled as follows, with the AX register used as an
  218. intermediary.
  219.  
  220.    A13800  MOV  AX,WORD PTR [Y%]     ;move Y% into AX
  221.    A33600  MOV  WORD PTR [X%],AX     ;move AX into X%
  222.  
  223. Assigning one long integer from another as in X& = Y& is handled similarly:
  224.  
  225.    A13A00    MOV  AX,WORD PTR [Y&]     ;move AX from Y& low
  226.    8B163C00  MOV  DX,WORD PTR [Y&+2]   ;move DX from Y& high
  227.    A33600    MOV  WORD PTR [X&],AX     ;move X& low from AX
  228.    89163800  MOV  WORD PTR [X&+2],DX   ;move X& high from DX
  229.  
  230. You may have noticed that instructions that use the AX registers require
  231. only three bytes to access a word of memory, while those that use DX (or
  232. indeed, any register other than AX) require four.  But don't be so quick
  233. to assume that BASIC is not optimizing your code.  The advantage to using
  234. separate registers is that the full value of Y& is preserved.  Had AX been
  235. used both times, the low word would be lost when the high word was
  236. transferred from Y& to X&.
  237.    When assigning one variable to many in a row, BASIC is smart enough to
  238. remember which values are in which registers, and it reuses those values
  239. for subsequent assignments.  The combination BASIC and assembly language
  240. code shown below was captured from a CodeView session and edited slightly
  241. for clarity.  It shows the actual assembly language code bytes generated
  242. for a series of assignments.
  243.  
  244. Plain integer assignments:
  245.  
  246.    A% = 1234
  247.        C7063600D204   MOV  WORD PTR [A%],&H04D2
  248.    B% = 1234
  249.        C7063800D204   MOV  WORD PTR [B%],&H04D2
  250.    C% = 1234
  251.        C7063A00D204   MOV  WORD PTR [C%],&H04D2
  252.    D% = 1234
  253.        C7063C00D204   MOV  WORD PTR [D%],&H04D2
  254.    E% = 1234
  255.        C7063E00D204   MOV  WORD PTR [E%],&H04D2
  256.  
  257.  
  258. Plain long integer assignments:
  259.  
  260.    V& = 1234
  261.        C7064000D204   MOV  WORD PTR [V&],&H04D2
  262.        C70642000000   MOV  WORD PTR [V&+2],0
  263.    W& = 1234
  264.        C7064400D204   MOV  WORD PTR [W&],&H04D2
  265.        C70642000000   MOV  WORD PTR [W&+2],0
  266.    X& = 1234
  267.        C7064800D204   MOV  WORD PTR [X&],&H04D2
  268.        C70642000000   MOV  WORD PTR [X&+2],0
  269.    Y& = 1234
  270.        C7064C00D204   MOV  WORD PTR [Y&],&H04D2
  271.        C70642000000   MOV  WORD PTR [Y&+2],0
  272.    Z& = 1234
  273.        C7065000D204   MOV  WORD PTR [Z&],&H04D2
  274.        C70642000000   MOV  WORD PTR [Z&+2],0
  275.  
  276.  
  277. Assigning multiple integers from another:
  278.  
  279.    A% = 1234
  280.        C7063600D204   MOV  WORD PTR [A%],&H04D2
  281.    B% = A%
  282.        A13600         MOV  AX,WORD PTR [A%]
  283.        A33800         MOV  WORD PTR [B%],AX
  284.    C% = A%
  285.        A33A00         MOV  WORD PTR [C%],AX
  286.    D% = A%
  287.        A33C00         MOV  WORD PTR [D%],AX
  288.    E% = A%
  289.        A33E00         MOV  WORD PTR [E%],AX
  290.  
  291.  
  292. Assigning multiple long integers from another:
  293.  
  294.    V& = 1234
  295.        C7064000D204   MOV  WORD PTR [V&],&H04D2
  296.        C70642000000   MOV  WORD PTR [V&+2],0
  297.    W& = V&
  298.        A14000         MOV  AX,WORD PTR [V&]
  299.        8B164200       MOV  DX,WORD PTR [V&+2]
  300.        A34400         MOV  WORD PTR [W&],AX
  301.        89164600       MOV  WORD PTR [W&+2],DX
  302.    X& = V&
  303.        A34800         MOV  WORD PTR [X&],AX
  304.        89164A00       MOV  WORD PTR [X&+2],DX
  305.    Y& = V&
  306.        A34C00         MOV  WORD PTR [Y&],AX
  307.        89164E00       MOV  WORD PTR [Y&+2],DX
  308.    Z& = V&
  309.        A35000         MOV  WORD PTR [Z&],AX
  310.        89165200       MOV  WORD PTR [Z&+2],DX
  311.  
  312.  
  313. The first five statements assign the value 1234 (04D2 Hex) to integer
  314. variables, and each requires six bytes of code.  The next five instructions
  315. assign the same value to long integers, taking two such instructions for
  316. a total of 12 bytes for each assignment.  Note that a zero is assigned to
  317. the higher word of each long integer, because the full Hex value being
  318. assigned is actually &H000004D2.  Simple multiplication shows that the five
  319. integer assignments generates five times six bytes, for a total of 30
  320. bytes.  The long integer assignments take twice that at 60 bytes total.
  321.    But notice the difference in the next two statement blocks.  The first
  322. integer assignment requires the usual six bytes, and the second does as
  323. well.  But thereafter, any number of additional integer variables will be
  324. assigned with only three bytes apiece.  Likewise, all but the first two
  325. long integer assignments are implemented using only seven bytes each. 
  326. Remembering what values are in each register is yet one more optimization
  327. that BASIC performs as it compiles your program.
  328.  
  329.  
  330. SHORT CIRCUIT EXPRESSION EVALUATION
  331.  
  332. Many programming situations require more than one test to determine if a
  333. series of instructions are to be executed or a branch taken.  The short
  334. example below tests that a string is not null, and also that the row and
  335. column to print at are legal.
  336.  
  337.    IF Work$ <> "" AND Row <= 25 AND Column <= 80 THEN
  338.      LOCATE Row, Column
  339.      PRINT Work$
  340.    END IF
  341.  
  342. When this program is compiled with QuickBASIC, all three of the tests are
  343. first performed in sequence, and the results are then combined to see if
  344. the LOCATE and PRINT should be performed.  The problem is that time is
  345. wasted comparing the row and column even if the string is null.  When speed
  346. is the primary concern, you should test first for the condition that is
  347. most likely to fail, and then use a separate test for the other conditions:
  348.  
  349.    IF Work$ <> "" THEN
  350.      IF Row <= 25 AND Column <= 80 THEN
  351.        LOCATE Row, Column
  352.        PRINT Work$
  353.      END IF
  354.    END IF
  355.  
  356. This separation of tests is called *short circuit expression evaluation*,
  357. because you are bypassing--or short circuiting--the remaining tests when
  358. the first fails.  Although it doesn't really take BASIC very long to
  359. determine if a string is null, the principle can be applied to other
  360. situations such as those that involve file operations like EOF and LOF. 
  361. Further, as you learned in Chapter 3, a better way to test for a non-null
  362. string is IF LEN(Work$) THEN.  However, the point is to perform those tests
  363. that are most likely to fail first, before others that are less likely or
  364. will take longer.
  365.    Another place where you will find it useful to separate multiple tests
  366. is when accessing arrays.  If you are testing both for a legal element
  367. number and a particular element value, QuickBASIC will give a "Subscript
  368. out of range" error if the element number is not valid.  This is shown
  369. below.
  370.  
  371.    IF Element <= MaxEls AND Array(Element) <> 0 THEN
  372.  
  373. Since QuickBASIC always performs both tests, the second will cause an error
  374. if Element is not a legal value.  In this case, you *have* to implement the
  375. tests using two separate statements:
  376.  
  377.    IF Element <= MaxEls THEN
  378.      IF Array(Element) <> 0 THEN
  379.       .
  380.       .
  381.    END IF
  382.  END IF
  383.  
  384. You may have noticed the I have referred to QuickBASIC here exclusively
  385. in this discussion.  Beginning with BASIC 7.0, Microsoft has added short
  386. circuit testing to the compiler as part of its built-in decision making
  387. process.  Therefore, when you have a statement such as this one:
  388.  
  389.    IF X > 1 AND Y = 2 AND Z < 3 THEN
  390.  
  391. BASIC PDS substitutes the following logic automatically:
  392.  
  393.    IF X <= 1 THEN GOTO SkipIt
  394.    IF Y <> 2 THEN GOTO SkipIt
  395.    IF Z >= 3 THEN GOTO SkipIt
  396.     .
  397.     .
  398.    SkipIt:
  399.  
  400. Speaking of THEN and GOTO, it is worth mentioning that the keyword THEN
  401. is not truly necessary when the only thing that follows is a GOTO.  That
  402. is, IF X < 1 GOTO Label is perfectly legal, although the only savings is
  403. in the program's source code.
  404.    This next and final trick isn't technically a short circuit expression
  405. test, but it can reduce the size of your programs in a similar fashion. 
  406. Chapter 3 compared the relative advantages of GOSUB routines and called
  407. subprograms, and showed that a subprogram is superior when passing
  408. parameters, while a GOSUB is much faster and smaller.  An ideal compromise
  409. in some situations is to combine the two methods.
  410.    If you have a called subprogram (or function) that requires a large
  411. number of parameters and it is called many times, you can use a single call
  412. within a GOSUB routine.  Since a GOSUB statement generates only three bytes
  413. of code each time it is used, this can be an ideal way to minimize the
  414. number of times that the full CALL is required.  Of course, GOSUB does not
  415. accept parameters, but many of them may be the same from call to call. 
  416. In particular, some third-party add-on libraries require a long series of
  417. arguments that are unlikely to change.  This is shown below.
  418.  
  419.  
  420. Row = 10
  421. Column = 20
  422. Message$ = "Slap me five"
  423. GOSUB DisplayMsg
  424.  .
  425.  .
  426. DisplayMsg:
  427. CALL ManyParams(Row, Column, Message$, MonType, NoSnow, FGColr, BGColr, _
  428.   HighlightFlag, VideoMode, VideoPage)
  429. RETURN
  430.  
  431.  
  432. In many cases you would have assigned permanent values for the majority
  433. of these parameters, and it is wasteful to have BASIC create code to pass
  434. them repeatedly.  Here, the small added overhead of the three assignments
  435. prior to each GOSUB results in less code than passing all ten arguments
  436. repeatedly.
  437.  
  438.  
  439. MISCELLANEOUS TIPS AND TECHNIQUES
  440. =================================
  441.  
  442. There are many tricks that programmers learn over the years, and the
  443. following are some of the more useful ones I have developed myself, or come
  444. across in magazines and other sources.
  445.  
  446.  
  447. FORMATTING AND ROUNDING
  448.  
  449. One frequent requirement in many programs is having control over how
  450. numbers are formatted.  Of course, BASIC has the PRINT USING statement
  451. which is adequate in most cases.  And Chapter 6 also showed how to trick
  452. BASIC's file handling statements into letting you access a string formatted
  453. by PRINT USING.  But there are other formatting issues that are not handled
  454. by BASIC directly.
  455.    One problem for many programmers is that BASIC adds leading and trailing
  456. blanks when printing numbers on the screen or to a disk file.  The leading
  457. blank is a placeholder for a possible minus sign, and is not added when the
  458. number is in fact negative.  Avoiding the trailing blank is easy; simply
  459. use PRINT STR$(Number).  And the easiest way to omit the leading blank for
  460. positive numbers is to use LTRIM$: PRINT LTRIM$(STR$(Number)).
  461.    PRINT USING is notoriously slow, because examines each character in a
  462. string version of the number, and reformat the digits while interpreting
  463. the many possible options specified in a separate formatting string.  But
  464. in many cases all that is really needed is simple right justification.  To
  465. right-align an integer value (or series of values) you can use RSET to
  466. assign the numbers into a string, and then print that string as shown
  467. below.
  468.  
  469.    Work$ = SPACE$(10)
  470.    REST Work$ = STR$(Number)
  471.    PRINT TAB(15); Work$
  472.  
  473. In this case, Work$ could also have been dimensioned as a fixed-length
  474. string.  Adding leading zeros to a number is also quite easy using RIGHT$
  475. like this:
  476.  
  477.    PRINT RIGHT$("00000" + LTRIM$(STR$(Number)), 6)
  478.  
  479. You will need at least as many zeros in the string as the final result
  480. requires, less one since STR$ always returns at least one digit.  Trailing
  481. digits are handled similarly, except you would use LEFT$ instead of RIGHT$.
  482.    Rounding numbers is an equally common need, and there are several ways
  483. to handle this.  Of course, INT and FIX can be used to truncate a floating
  484. point value to an integer result, but neither of these perform rounding. 
  485. For that you should use CINT or CLNG, which do round the number to the
  486. closest integer value.  For example, Value = CINT(3.59) will assign 4 to
  487. Value, regardless of whether Value is an integer, single precision, or
  488. whatever.
  489.    Some BASICs have a CEIL function, which returns the next *higher* integer
  490. result.  That is, CEIL(3) is 3, but CEIL(3.01) returns the value 4.  This
  491. function can be easily simulated using Ceil = -INT(-Number).
  492.    Rounding algorithms are not quite so simple to implement, as you can see
  493. in the short DEF FN function below.
  494.  
  495.    DEF FnRound# (Value#, Digits%)
  496.      Mult% = 10 ^ Digits%
  497.      FnRound# = FIX((Mult% * Value#) + (SGN(Value#)) * .5#) / Mult%
  498.    END DEF
  499.  
  500. Another important math optimization is to avoid exponentiation whenever
  501. possible.  Whether you are using integers or floating point numbers, using
  502. Number ^ 2 and Number ^ 3 are many times slower than Number * Number and
  503. Number * Number * Number respectively.
  504.  
  505.  
  506. STRING TRICKS AND MINIMIZING PARAMETERS
  507.  
  508. There are a few string tricks and issues worth mentioning here too.  The
  509. fastest and smallest way to clear a string without actually deleting it is
  510. with LSET Work$ = "".  Another clever and interesting string trick lets you
  511. delete a string with only nine bytes of code, instead of the usual 13.
  512.    In Chapter 6 you learned that the assembly language routines within
  513. BASIC's runtime library are accessible if you know their names.  You can
  514. exploit that by using the B$STDL (string delete) routine, which requires
  515. less code to set up and call than the more usual Work$ = "".  When a string
  516. is assigned to a null value, two parameters--the address of the target
  517. string and the address of the null--are passed to the string assignment
  518. routine.  But B$STDL needs only the address of the string being deleted. 
  519. You might think that BASIC would be smart enough to see the "" null and
  520. call B$STDL automatically, but it doesn't.  Here is how you would declare
  521. and call B$STDL:
  522.  
  523.    DECLARE SUB DeleteStr ALIAS "B$STDL" (Work$)
  524.    CALL DeleteStr(Any$)
  525.  
  526. As with the examples that let you call GET # and PUT # directly, DeleteStr
  527. will not work in the QB environment unless you first create a wrapper
  528. subprogram written in BASIC, and include that wrapper in a Quick Library. 
  529. And this brings up an important point.  Why bother to write a BASIC
  530. subprogram that in turn calls an internal routine, when the BASIC
  531. subprogram could just as easily delete the string itself?  Therefore, the
  532. best solution--especially because it's also the easiest--is to write
  533. DeleteStr in BASIC thus:
  534.  
  535.    SUB DeleteStr(Work$)
  536.      Work$ = ""
  537.    END SUB
  538.  
  539. This is an important concept to be sure, because it shows how to reduce
  540. the number of parameters when a particular service is needed many times. 
  541. Other similar situations are not hard to envision, whereby multiple
  542. parameters that do not change from call to call can be placed into a
  543. subprogram that itself requires only one or two arguments.
  544.    This technique can be extended to several BASIC statements that use more
  545. parameters than might otherwise be apparent.  For example, whenever you use
  546. LOCATE, additional hidden parameters are passed to the B$LOCT routine
  547. beyond those you specify.  The statement LOCATE X, Y generates 22 bytes of
  548. code, even though other called routines that take two parameters need only
  549. 13.  (Every passed parameter generates four bytes of code, and the actual
  550. CALL adds five more.  This is the same whether the routine being called is
  551. an internal BASIC statement, a BASIC subprogram or function, or an assembly
  552. language routine.)  Therefore, if you use LOCATE with two arguments
  553. frequently in a program, you can save nine bytes for each by creating a
  554. BASIC subprogram that performs the LOCATE:
  555.  
  556.    SUB LocateIt(Row, Column) STATIC
  557.      LOCATE Row, Column
  558.    END SUB
  559.  
  560. Similarly, if you frequently turn the cursor on and off, you should create
  561. two subprograms--perhaps called CursorOn and CursorOff--that invoke LOCATE. 
  562. Since no parameters are required, the savings will add up quickly.  Calling
  563. either of the subprograms below generates only five bytes of code, as
  564. opposed to 18 for the statement LOCATE , , 1 and 20 for LOCATE , , 0.
  565.  
  566.    SUB CursorOn STATIC
  567.      LOCATE , , 1
  568.    END SUB
  569.  
  570.    SUB CursorOff STATIC
  571.      LOCATE , , 0
  572.    END SUB
  573.  
  574. The COLOR statement also requires more parameters than the number of
  575. arguments you give.  Where COLOR FG, BG generates 22 bytes of compiled
  576. code, CALL ColorIt(FG, BG) creates only 13.  CLOSE is yet another BASIC
  577. statement that accepts multiple arguments, and it too requires hidden
  578. parameters.  Using CLOSE #X compiles to 13 bytes, and CALL CloseIt(X) is
  579. only nine.
  580.    The reason that BASIC sends more parameters than you specify is because
  581. these routines need extra information to know which and how many arguments
  582. were given.  In the case of LOCATE, each argument is preceded with a flag
  583. that tells if the next one was given.  CLOSE is similar, except the last
  584. parameter tells how many file numbers were specified.  Remember, you can
  585. use CLOSE alone to close all open files, or CLOSE 1, 3, 4 to close only
  586. those files numbers.  Therefore, BASIC requires some way to tell the CLOSE
  587. statement how many file numbers there are.
  588.    Another place where several statements can be consolidated within a
  589. single procedure is when peeking and poking memory.  BASIC's PEEK and POKE
  590. are limited because they can access only one byte in memory at a time.  But
  591. many useful memory locations are in fact organized as a pair of bytes, as
  592. you will see in Chapter 10.  Instead of using code to combine or separate
  593. the bytes each time memory is accessed, you can use the following short
  594. routines that let you peek and poke two bytes at once.
  595.  
  596.  
  597. DECLARE FUNCTION PeekWord%(Address%)
  598.   PeekWord% = PEEK(Address%) + 256 * PEEK(Address% + 1)
  599. END FUNCTION
  600.  
  601. DECLARE SUB PokeWord(Address%, Value%)
  602.   POKE Address%, Value% AND 255
  603.   POKE Address% + 1, Value% \ 256
  604. END SUB
  605.  
  606.  
  607. Because these routines use BASIC's PEEK and POKE, you still need to use DEF
  608. SEG separately.  Of course, the segment could be added as another
  609. parameter, and assigned within the routines:
  610.  
  611.  
  612. DECLARE FUNCTION PeekWord%(Segment%, Address%)
  613.   DEF SEG = Segment%
  614.   PeekWord% = PEEK(Address%) + 256 * PEEK(Address% + 1)
  615. END FUNCTION
  616.  
  617.  
  618. WORD WRAPPING
  619.  
  620. A string handling technique you will surely find useful is implementing
  621. word wrapping.  There are a number of ways to do this, and the following
  622. code shows one that I have found to be very efficient.
  623.  
  624. DEFINT A-Z
  625. SUB WordWrap (X$, Wide, LeftMargin)
  626.  
  627.   Length = LEN(X$)   'remember the length
  628.   Pointer = 1        'start at the beginning of the string
  629.   IF LeftMargin = 0 THEN LeftMargin = 1
  630.  
  631.   'Scan a block of Wide characters backwards, looking for a blank.  Stop
  632.   '  at the first blank, or upon reaching the beginning of the string.
  633.   DO
  634.     FOR X = Pointer + Wide TO Pointer STEP -1
  635.       IF MID$(X$, X, 1) = " " OR X = Length + 1 THEN
  636.         LOCATE , LeftMargin
  637.         PRINT MID$(X$, Pointer, X - Pointer);
  638.         Pointer = X + 1
  639.         WHILE MID$(X$, Pointer, 1) = " "
  640.           Pointer = Pointer + 1
  641.         WEND
  642.         IF POS(0) > 1 THEN PRINT
  643.         EXIT FOR
  644.       END IF
  645.     NEXT
  646.   LOOP WHILE Pointer < Length
  647. END SUB
  648.  
  649. The WordWrap subprogram expects the text for display to be in a single
  650. long string.  You pass it that text, a left margin, and a width.  You could
  651. certainly add enhancements to this routine such as a color parameter, or
  652. the ability to format the text and send it to a printer or disk file.
  653.  
  654.  
  655. UNUSUAL WAYS TO ACCESS DISPLAY MEMORY
  656.  
  657. If you ever tried to print a character in the lower-right corner of the
  658. display screen, you probably discovered that it cannot be done [with many
  659. BASIC versions] without causing the screen to scroll up.  The only solution
  660. I am aware of is to use POKE to assign the character (and optionally its
  661. color) to display memory directly as shown below.
  662.  
  663.    DEF SEG = &HB800     'use &HB000 for a monochrome display
  664.    POKE 3998, 65        'ASCII code for the letter "A"
  665.    POKE 3999, 9         'bright blue on black
  666.  
  667. The second trick also uses display memory in an unconventional manner. 
  668. All video adapters contain at least 4096 bytes of on-board memory.  Even
  669. though a 25 line by 80 column text mode screen uses only 4000 bytes (2000
  670. characters plus 2000 colors), memory chips are built in multiples of 1,024
  671. bytes.  Therefore, you can use the last 96 bytes on the display adapter
  672. in your programs.  If the adapter supports multiple video pages, then you
  673. can use the last 96 bytes in each 25-line page.
  674.    One use for this memory is to provide a way to communicate small amounts
  675. of information between separate programs.  When you don't want to structure
  676. an application to use CHAIN, the only other recourse is to use a disk file
  677. to pass information between the programs.  But if all that is needed is a
  678. file name or drive letter, using a file can be awkward and slow, especially
  679. if the program is running from a floppy disk.
  680.    One way to access this video memory is with PEEK and POKE.  But PEEK and
  681. POKE are awkward too, and can access only one byte at a time.  A better
  682. approach is to use an assembly language routine to copy one contiguous
  683. memory block to another location.  The MemCopy routine below is designed
  684. to do exactly this.
  685.  
  686. ;MEMCOPY.ASM, copies a block of memory from here to there
  687.  
  688. .Model Medium, Basic
  689. .Code
  690.  
  691. MemCopy Proc Uses DS ES SI DI, FromAdr:DWord, ToAdr:DWord, NumBytes:Word
  692.  
  693.   Cld               ;copy in the forward direction
  694.  
  695.   Mov  SI,NumBytes  ;get the address for NumBytes%
  696.   Mov  CX,[SI]      ;put it into CX for copying below
  697.  
  698.   Les  DI,FromAdr   ;load ES:DI with the source address
  699.   Lds  SI,ToAdr     ;load DS:SI with destination address
  700.  
  701.   Shr  CX,1         ;copy words instead of bytes for speed
  702.   Rep  Movsw        ;do the copy
  703.   Adc  CX,CX        ;this will set CX to either 0 or 1
  704.   Rep  Movsb        ;copy the odd byte if necessary
  705.  
  706.   Ret               ;return to BASIC
  707.  
  708. MemCopy Endp
  709. End
  710.  
  711. MemCopy may be declared and called in two different ways.  The first uses
  712. SEG and is most appropriate when you are copying data between variables,
  713. for example from a group of elements in one array to elements in another. 
  714. The second lets you specify any arbitrary segment and address, and it
  715. requires the BYVAL modifier either in the DECLARE statement, the CALL, or
  716. both.  Each method is shown below.
  717.  
  718.    DECLARE SUB MemCopy(SEG AnyVar1, SEG AnyVar2, Numbytes%)
  719.    CALL MemCopy(AnyVar1, AnyVar2, NumBytes%)
  720.  
  721.    DECLARE SUB MemCopy(BYVAL Seg1%, BYVAL Adr1%, BYVAL Seg2%, _
  722.      BYVAL Adr2%, NumBytes%)
  723.    CALL MemCopy(SourceSeg%, SourceAdr%, DestSeg%, DestAdr%, NumBytes%)
  724.  
  725. You may also use a combination of these, perhaps with SEG for the source
  726. argument and BYVAL for the second.  For example, to copy a 20-byte TYPE
  727. variable to the area just past the end of video memory on a color display
  728. adapter you would do this:
  729.  
  730.    CALL MemCopy(SEG TypeVar, BYVAL &HB800, BYVAL 4000, 20)
  731.  
  732. In many cases you may need to use MemCopy in more than one way in the same
  733. program.  For this reason it is probably better not to declare it at all. 
  734. Once a subprogram or function has been declared, BASIC will refuse to let
  735. you change the number or type of parameters.  But if you don't include a
  736. declaration at all, you are free to use any combination of SEG and BYVAL,
  737. and also any type of variable.
  738.    It is important to understand that numeric and TYPE variables should be
  739. specified using SEG, so MemCopy will know the full address where the
  740. variable resides.  You could use a combination of BYVAL VARSEG(Variable)
  741. and BYVAL VARPTR(Variable), but that is not quite as efficient as SEG. 
  742. Copying to or from a conventional string using QuickBASIC requires SADD
  743. (string address) instead of VARPTR; far strings in BASIC 7 require SADD,
  744. and also SSEG (string segment) instead of VARSEG.
  745.  
  746.  
  747. REBOOTING A PC
  748.  
  749. Another simple trick that is not obvious to many programmers is how to
  750. reboot a PC.  Although most PC technical reference manuals show an
  751. interrupt service for rebooting, that simply does not work with most
  752. computers.  However, every PC has a BIOS routine that is at a fixed
  753. address, and which may be called directly like this:
  754.  
  755.    DEF SEG = &HFFFF
  756.    CALL Absolute(0)
  757.  
  758. The Absolute routine is included in thee QB and QBX libraries that come with
  759. BASIC.  If a cold boot with the full memory test and its attendant delay
  760. is acceptable, then the code shown above is all that you need.  Otherwise,
  761. you must poke the special value &H1234 in low memory as a flag to the BIOS
  762. routine, so it will know that you want a warm boot instead:
  763.  
  764.    DEF SEG = 0
  765.    POKE &H473, &H12
  766.    POKE &H472, &H34
  767.    DEF SEG = &HFFFF
  768.    CALL Absolute(0)
  769.  
  770.  
  771. INTEGER VALUES GREATER THAN 32K
  772.  
  773. As you learned in Chapter 2, an integer variable can hold any value between
  774. -32768 and 32767.  When this range of numbers is considered, the integer
  775. is referred to as being a signed number.  But the same range of values can
  776. also be treated as unsigned numbers spanning from 0 through 65535.  Since
  777. BASIC does not support unsigned integers, additional trickery is often
  778. needed to pass values between 32768 and 65535 to assembler routines and DOS
  779. and BIOS services you invoke with CALL Interrupt.  One way to do this is
  780. to use a long integer first, and add an explicit test for values higher
  781. than 32767:
  782.  
  783.    Temp& = NumBytes&
  784.    IF Temp& > 32767 THEN
  785.      IntBytes% = Temp& - 65536
  786.    ELSE
  787.      IntBytes% = Temp&
  788.    END IF
  789.  
  790. To reverse the process you would test for a negative value:
  791.  
  792.    IF IntBytes% < 0 THEN
  793.      NumBytes& = IntBytes% + 65536
  794.    ELSE
  795.      NumBytes& = IntBytes%
  796.    END IF
  797.  
  798. Although this method certainly works, it is inefficient because of the
  799. added IF testing.  When you merely need to pass a variable to a called
  800. routine, you can skip this testing and simply pass the long integer
  801. directly.  This may appear counter to the rule that you must always pass
  802. the same type of variable that a subroutine expects.  But as long as the
  803. arguments are not being passed by value using BYVAL, this method works and
  804. adds no extra code.
  805.    When a parameter is passed to a subprogram or function, BASIC sends the
  806. address of its first byte as shown in Figure 9-1.
  807.  
  808.  
  809. ─ ─ ─ ┬────┬────┬────┬────┬ ─ ─ ─
  810.       │ B1 │ B2 │ B3 │ B4 │
  811. ─ ─ ─ ┴────┴────┴────┴────┴ ─ ─ ─
  812.       ^
  813.       └───────── Address passed to the routine
  814.  
  815. Figure 9-1: Passing a long integer where a regular integer is expected.
  816.  
  817.  
  818. Here, B1, B2, and so forth refer to the Bytes 1 through 4 of a long integer
  819. variable.  Since the assembly language routine is expecting a regular
  820. integer, it looks at just the first two bytes of the variable.  Thus, a
  821. long integer can be used even when a conventional integer is expected.  Of
  822. course, any excess greater than 65535 will be ignored by the routine, since
  823. the bits that hold the excess are in the third and fourth bytes.
  824.  
  825.  
  826. BENCHMARKING
  827. ============
  828.  
  829. Throughout this book I have emphasized the importance of writing code that
  830. is as small and fast as possible.  And these goals should be obvious to all
  831. but the most novice programmer.  But it is not always obvious how to
  832. determine for yourself which of several approaches yields code that is the
  833. smallest or fastest.  One way is to use Microsoft CodeView, which lets you
  834. count the bytes of assembler code that are generated.  This is how I
  835. obtained the byte counts stated throughout this book.
  836.    But smaller is not always faster.  Further, the code that BASIC
  837. generates is not the whole story.  In many cases BASIC makes calls to its
  838. runtime library routines, and you would have to trace through those as well
  839. to know the total byte count for a given statement.  It is not impossible
  840. to trace through the BASIC runtime using CodeView, but it certainly can be
  841. tedious.  Many of BASIC's internal routines are very convoluted--especially
  842. those that allocate and deallocate string and other memory.  Often it is
  843. simpler to devise a test that executes a series of statements many times,
  844. and then time how long the test took.
  845.    As an example for this discussion, I will compare two different ways to
  846. print three strings in succession and show how to tell which produces less
  847. code, and which is faster.  The first statement below prints each string
  848. separately, and the second combines the strings and then prints them as
  849. one.
  850.  
  851.    1: PRINT X$; Y$; Z$
  852.    2: PRINT X$ + Y$ + Z$
  853.  
  854. Since the length of each string will certainly influence how long it takes
  855. to print them, each of the strings is first initialized to 80 characters
  856. as follows:
  857.  
  858.    X$ = STRING$(80, "X")
  859.    Y$ = STRING$(80, "Y")
  860.    Z$ = STRING$(80, "Z")
  861.  
  862. It is important to understand that the PRINT statement itself will be a
  863. factor, since it takes a certain amount of time to copy the characters
  864. from each string to display memory.  Worse, if the screen needs to be
  865. scrolled because the text runs past the bottom of the display, that will
  866. take additional time.  To avoid the overhead of scrolling, the test program
  867. uses LOCATE to start each new print statement at the top of the screen. 
  868. Of course, using LOCATE adds further to the overhead, but in this case much
  869. less than scrolling would.  To prove this to yourself, disable the line
  870. that contains the LOCATE statement.  Here's the complete benchmark program:
  871.  
  872. CLS
  873. X$ = STRING$(80, "X")    'create the test string
  874. Y$ = STRING$(80, "Y")
  875. Z$ = STRING$(80, "Z")
  876.  
  877. Synch! = TIMER           'synchronize to TIMER
  878. DO
  879.   Start! = TIMER
  880. LOOP WHILE Start! = Synch!
  881.  
  882. FOR X = 1 TO 1000        '1000 times is adequate
  883.   LOCATE 1
  884.   PRINT X$; Y$; Z$
  885. NEXT
  886.  
  887. Done! = TIMER            'calculate elapsed time
  888. Test1! = Done! - Start!
  889.  
  890. Synch! = TIMER           'as above
  891. DO
  892.   Start! = TIMER
  893. LOOP WHILE Start! = Synch!
  894.  
  895. FOR X = 1 TO 1000
  896.   LOCATE 1
  897.   PRINT X$ + Y$ + Z$
  898. NEXT
  899.  
  900. Done! = TIMER
  901. Test2! = Done! - Start!
  902.  
  903. PRINT USING "##.## seconds using three strings"; Test1!
  904. PRINT USING "##.## seconds using concatenation"; Test2!
  905.  
  906. Notice the extra step that synchronizes the start of each test to BASIC's
  907. TIMER function.  As you probably know, the PC's system time is updated
  908. approximately 18 times per second.  Therefore, it is possible that the test
  909. loop could begin just before the timer is about to be incremented.  In that
  910. case the elapsed time would appear to be 1/18th second longer than the
  911. actual time.  To avoid this potential inaccuracy, the DO loop waits until
  912. a new time period has just begun.  There is still a similar accuracy loss
  913. at the end of the test when Done! is assigned from TIMER.  But by
  914. synchronizing the start of the test, the error is limited to 1/18th second
  915. instead of twice that.
  916.    When you compile and run this program using QuickBASIC 4.5, it will be
  917. apparent that the first test is more than three times faster than the
  918. second.  However, with BASIC 7.1--using either near or far strings--the
  919. second is in fact slightly faster.  Therefore, which is better depends on
  920. the version of your compiler, and there is no single best answer.  Now
  921. let's compare code size.
  922.    The disassemblies shown below are valid for both QuickBASIC 4.5 and
  923. BASIC 7.1.  By counting bytes you can see that printing the strings using
  924. a semicolon generates 27 bytes, while first concatenating the strings
  925. requires 29 bytes.
  926.  
  927.  
  928. PRINT X$; Y$; Z$
  929.   B83600          MOV   AX,X$   ;get the address for X$
  930.   50              PUSH  AX      ;pass it on
  931.   9AD125FF4A      CALL  B$PSSD  ;print with a semicolon
  932.   B83A00          MOV   AX,Y$   ;as above for Y$
  933.   50              PUSH  AX
  934.   9AD125FF4A      CALL  B$PSSD
  935.   B83E00          MOV   AX,Z$
  936.   50              PUSH  AX
  937.   9AD625FF4A      CALL  B$PESD  ;print with end of line
  938.  
  939. PRINT X$ + Y$ + Z$
  940.   B83600          MOV   AX,X$   ;get the address for X$
  941.   50              PUSH  AX      ;pass it on
  942.   B83A00          MOV   AX,Y$   ;get the address for Y$
  943.   50              PUSH  AX      ;pass that on too
  944.   9AD728FF4A      CALL  B$SCAT  ;call String Concatenate
  945.   50              PUSH  AX      ;pass the combined result
  946.   B83E00          MOV   AX,Z$   ;get the address for Z$
  947.   50              PUSH  AX      ;pass it on
  948.   9AD728FF4A      CALL  B$SCAT  ;combine that too
  949.   50              PUSH  AX      ;pass X$ + Y$ + Z$
  950.   9AD625FF4A      CALL  B$PESD  ;print with end of line
  951.  
  952.  
  953. Even though the first example uses a single PRINT statement, BASIC treats
  954. it as three separate commands:
  955.  
  956.    PRINT X$;
  957.    PRINT Y$;
  958.    PRINT Z$
  959.  
  960. The second example that concatenates the strings requires slightly more
  961. code because of the repeated calls to the B$SCAT (string concatenate)
  962. routine.  Therefore, if you are using QuickBASIC it is clear that printing
  963. the strings separately is both smaller and faster.  BASIC PDS users must
  964. decide between slightly faster performance, or slightly smaller code.
  965.    These tests were repeated 1000 times to minimize the inaccuracies
  966. introduced by the timer's low resolution.  Since this method of timing can
  967. be off by as much as 1/18th second (55 milliseconds), for test results to
  968. be accurate to 1% the test must take at least 5.5 seconds to complete. 
  969. In most cases that much precision is not truly necessary, and other factors
  970. such as the time to use LOCATE will prevent absolute accuracy anyway.
  971.    It is important that any timing tests you perform be done after
  972. compiling the program to an .EXE file.  The BASIC editor is an interpreter,
  973. and is generally slower than a stand-alone program.  Further, the reduction
  974. in speed is not consistent; some statements are nearly as fast as in a
  975. compiled program, and some are much slower.
  976.    To obtain more accurate results than those shown here requires some
  977. heavy ammunition; I recommend the Source Profiler from Microsoft.  This
  978. is a utility program that times procedure calls within a running program
  979. to an accuracy of one microsecond.  The Source Profiler supports all
  980. Microsoft languages including QuickBASIC and BASIC PDS.
  981.    To time a program you must compile and link it using the /zi and /co
  982. CodeView switches.  This tells BASIC and LINK to add symbolic information
  983. that shows where variables and procedures are located, and also relates
  984. each logical line of source code to addresses in the .EXE file.  The Source
  985. Profiler then uses this information to know where each source-language
  986. statement begins and ends.
  987.    You should also understand that there's a certain amount of overhead
  988. associated with the timing loop itself.  Any FOR/NEXT loop requires a
  989. certain amount of time just to increment the counter variable and compare
  990. it to the ending value.  Fortunately, this overhead can be easily isolated,
  991. using an empty loop with the same number of iterations.  The short complete
  992. program that follows shows this in context.
  993.  
  994. Synch! = TIMER
  995. DO
  996.   Start! = TIMER
  997. LOOP WHILE Start! = Synch!
  998.  
  999. FOR X& = 1 TO 50000
  1000. NEXT
  1001.  
  1002. Done! = TIMER
  1003. Empty! = Done! - Start!
  1004. PRINT USING "##.## seconds for the empty loop"; Empty!
  1005.  
  1006. Synch! = TIMER
  1007. DO
  1008.   Start! = TIMER
  1009. LOOP WHILE Start! = Synch!
  1010.  
  1011. FOR X& = 1 TO 50000
  1012.   X! = -Y!
  1013. NEXT
  1014.  
  1015. Done! = TIMER
  1016. Assign! = Done! - Start!
  1017. PRINT USING "##.## seconds for the assignments"; Assign!
  1018.  
  1019. Actual! = Assign! - Empty!
  1020. PRINT USING "##.## seconds actually required"; Actual!
  1021.  
  1022. SUMMARY
  1023. =======
  1024.  
  1025. In this chapter you learned a variety of programming shortcuts and other
  1026. techniques.  You saw firsthand how it is more efficient to avoid using CHR$
  1027. and other BASIC functions repeatedly, in favor a single call ahead of time
  1028. when possible.  In a similar vein, you can reduce the size of your programs
  1029. by consolidating multiple instances of UCASE$, MID$, LTRIM$, and other
  1030. functions once before a series of IF tests, rather than use them each time
  1031. for each test.
  1032.    You also learned that assigning multiple variables in succession from
  1033. another often results in smaller code than assigning from the same numeric
  1034. constant.  Short circuit expression evaluation was described, and examples
  1035. showed you how that technique can improve the efficiency of a QuickBASIC
  1036. program.  But since BASIC PDS already employs this optimization, multiple
  1037. AND conditions are not needed when using that version of compiler.
  1038.    This chapter explained the importance of reducing the number of
  1039. parameters you pass to a subprogram or function, and showed how you can use
  1040. GOSUB to invoke a central handler that in turn calls the routine. 
  1041. Likewise, when using BASIC statements such as LOCATE, COLOR, and CLOSE that
  1042. require additional arguments beyond those you specify, a substantial amount
  1043. of code can be saved by creating a BASIC subprogram wrapper.  Examples for
  1044. turning the cursor on and off were shown, and these can save 13 and 15
  1045. bytes per use respectively.
  1046.    Several programming techniques were shown, including a word wrap
  1047. subprogram, a numeric rounding function, and a simple way to reboot the PC. 
  1048. You also learned how small amounts of data can be safely stored in the last
  1049. 96 bytes of video memory, perhaps for use as a common data area between
  1050. separately run [non-chained] programs.
  1051.    Finally, this chapter included a brief discussion of some of the issues
  1052. surrounding benchmarking, and explained how to obtain reasonably accurate
  1053. statement timings.  To determine the size of the compiler-generated code
  1054. requires disassembling with CodeView.
  1055.    Chapter 10 continues with a complete list of key addresses in low memory
  1056. you are sure to find useful, and discusses each in depth along with
  1057. accompanying examples.
  1058.