dan@srs.UUCP (Dan Kegel) (10/31/86)
Quick console output on MS-DOS (or, It's Better When It's Raw) The "Inside the IBM PC" issue of BYTE (October 1986) makes it painfully obvious that many people, contributing authors to BYTE included, still don't know that MS-DOS is capable of extremely high speed I/O to/from character devices. This note tries to remedy this by showing an estimate of the delays involved in performing output to a character device, and shows how to speed up output by a factor of 10 to 100. Writing to standard output when the device driver ANSI.SYS is installed costs about (N * 1700) microseconds (on a 4.77 MHz 8088 system), where N is the number of characters written (measured without scrolling). What takes so long? Usually, when DOS is called to write N bytes to the console, it sits in a tight loop: dos_write() { for each character { call device driver write routine to write 1 character check for ^C by calling device driver status routine } } However, if the device has been put into RAW mode with the DOS IOCTL function call [footnote 2], DOS will handle a write request differently: dos_write() { call device driver write routine to write N characters } This gets rid of the overhead of 1-2 device driver calls per character, and moves the inner loop to the inside of the device driver. Setting RAW mode brings the cost of writing to ANSI.SYS down to about (600 + N * 1100) microseconds. That's better, but still not very good. Disassembly of ANSI.SYS shows that its inner loop is slow mostly because it calls BIOS to write to the screen- and BIOS calls are notoriously slow. What can be done? The best solution is to write an efficient device driver which bypasses BIOS, and writes directly to video memory. This has been done [footnote 1]; writing to such a device driver in RAW mode takes about (1000 + N * 70) microseconds per character. Note that these measurements were made under non-scrolling conditions; scrolling is (usually) a fairly slow process. Fortunately, most screen- intensive applications- for example, games, spreadsheets, and text editors- can usually avoid scrolling the screen. (Advanced ANSI functions such as Insert N Lines and Delete N Lines, provided by both the device drivers in footnote 1, can make it easier to avoid scrolling.) These measurements also assume that the entire write is done with a single call to DOS; if your program calls DOS once for each character written, output will be extremely slow whether or not the device is in RAW mode. If you are using C, it helps to use buffered output rather than unbuffered output; usually (on MS-DOS) stdout defaults to being unbuffered. You can force it to be buffered with the following code: #include <stdio.h> static char stdoutbuf[BUFSIZ]; main() { setbuf(stdout, stdoutbuf); ... } If you do this, you will have to call fflush(stdout) whenever you finish updating a screen or after writing a user prompt. In short, all that is needed to achieve high-speed output to devices is (1) set RAW mode during output [see footnote 2], (2) use the proper device driver [footnote 1], (3) avoid scrolling, and (4) write more than one character per DOS call, preferably several lines' worth (which, in C, means using setbuf(), fflush(), and buffered output). --- Daniel Kegel ...seismo!rochester!srs!dan --------------------------------------- [Footnote 1] The device driver FANSI, aka FCONSOLE.DEV, is a fancy commercial product which offers speedy output during RAW mode. It is advertized in BYTE. The device driver NANSI.SYS is a simpler program in the public domain which has the advantage that it is free and comes with sources; it is available occasionally over Usenet (on net.micro.pc), and probably on Compuserve. Both of these drivers offer many features not found in the standard ANSI.SYS, which was written by somebody who loathed speed and functionality. --------------------------------------- [Footnote 2] Setting RAW mode The following Microsoft C subroutines can be used to set or reset RAW mode on the device(s) associated with stdin and stdout (usually, the console). Note that RAW mode is not really intended to be left on all the time; when RAW mode is on, echoing and line editing is disabled- which can cause some programs to appear to crash when asking for input from the console. Applications should always restore the RAWness of stdin/stdout when exiting. /*--- setraw.c ------------------------------------------------- Routines to set and reset raw mode on stdin/stdout for Microsoft C. Stolen from the sources to the mighty game Hack. Thanks to Mark Zbikowski (markz@microsoft.UUCP). ------------------------------------------------------------------*/ #include <dos.h> #include <stdio.h> #define DEVICE 0x80 #define RAW 0x20 #define IOCTL 0x44 #define STDIN fileno(stdin) #define STDOUT fileno(stdout) #define GETBITS 0 #define SETBITS 1 static unsigned old_stdin, old_stdout, ioctl(); #define BREAKCHECK 0x33 static unsigned old_breakchk, breakctl(); /*--- set_raw --------------------------------------------------------- Call this to set raw mode; call restore_raw() later to restore console to old rawness state. (Don't strand user in RAW mode!) Not only sets raw mode, but also turns off ^C trapping on random DOS calls. All character input from stdin should be done with DOS fn call 7 to avoid ^C trapping on input. -----------------------------------------------------------------------*/ set_raw() { old_stdin = ioctl(STDIN, GETBITS, 0); old_stdout = ioctl(STDOUT, GETBITS, 0); old_breakchk = breakctl(GETBITS, 0); if (old_stdin & DEVICE) ioctl(STDIN, SETBITS, old_stdin | RAW); if (old_stdout & DEVICE) ioctl(STDOUT, SETBITS, old_stdout | RAW); (void) breakctl(SETBITS, 0); } restore_raw() { if (old_stdin) (void) ioctl(STDIN, SETBITS, old_stdin); if (old_stdout) (void) ioctl(STDOUT, SETBITS, old_stdout); if (old_breakchk) (void) breakctl(SETBITS, old_breakchk); } static unsigned ioctl(handle, mode, setvalue) unsigned setvalue; { union REGS regs; regs.h.ah = IOCTL; regs.h.al = mode; regs.x.bx = handle; regs.h.dl = setvalue; regs.h.dh = 0; /* Zero out dh */ intdos(®s, ®s); return (regs.x.dx); } /* Being sensitive to ^C is quite hazardous for a text editor */ /* This could and perhaps should have been done via the SIGNAL mechanism */ static unsigned breakctl(mode, setvalue) unsigned setvalue; { union REGS regs; regs.h.ah = BREAKCHECK; regs.h.al = mode; regs.h.dl = setvalue; intdos(®s, ®s); return (regs.x.dx & 0xff); } /*------ end of setraw.c ----------------------------------------------*/