This commit is contained in:
James Gregory 2013-12-30 11:58:19 +11:00
commit b946ff9e70
683 changed files with 62902 additions and 0 deletions

83
about.html Normal file
View file

@ -0,0 +1,83 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Foreword</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=Foreword//-->
<!--PAGES=xxxi-xxxii//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="intro.html">Previous</A></TD>
<TD><A HREF="index.html">Table of Contents</A></TD>
<TD><A HREF="about_author.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H2><A NAME="Heading1"></A><FONT COLOR="#000077">Foreword</FONT></H2>
<P>I got my start programming on Apple II computers at school, and almost all of my early work was on the Apple platform. After graduating, it quickly became obvious that I was going to have trouble paying my rent working in the Apple II market in the late eighties, so I was forced to make a very rapid move into the Intel PC environment.
</P>
<P>What I was able to pick up over several years on the Apple, I needed to learn in the space of a few months on the PC.</P>
<P>The biggest benefit to me of actually making money as a programmer was the ability to buy all the books and magazines I wanted. I bought a lot. I was in territory that I new almost nothing about, so I read <I>everything</I> that I could get my hands on. Feature articles, editorials, even advertisements held information for me to assimilate.</P>
<P>John Romero clued me in early to the articles by Michael Abrash. The good stuff. Graphics hardware. Code optimization. Knowledge and wisdom for the aspiring developer. They were even fun to read. For a long time, my personal quest was to find a copy of Michael&#146;s first book, <I>Zen of Assembly Language.</I> I looked in every bookstore I visited, but I never did find it. I made do with the articles I could dig up.</P>
<P>I learned the dark secrets of the EGA video controller there, and developed a few neat tricks of my own. Some of those tricks became the basis for the Commander Keen series of games, which launched id Software.</P>
<P>A year or two later, after Wolfenstein-3D, I bumped into Michael (in a virtual sense) for the first time. I was looking around on M&T Online, a BBS run by the Dr. Dobb&#146;s publishers before the Internet explosion, when I saw some posts from the man himself. We traded email, and for a couple months we played tag-team gurus on the graphics forum before Doom&#146;s development took over my life.</P>
<P>A friend of Michael&#146;s at his new job put us back in touch with each other after Doom began to make its impact, and I finally got a chance to meet up with him in person.</P>
<P>I talked myself hoarse that day, explaining all the ins and outs of Doom to Michael and an interested group of his coworkers. Every few days afterwards, I would get an email from Michael asking for an elaboration on one of my points, or discussing an aspect of the future of graphics.</P>
<P>Eventually, I popped the question&#151;I offered him a job at id. &#147;Just think: no reporting to anyone, an opportunity to code all day, starting with a clean sheet of paper. A chance to do <I>the right thing</I> as a programmer.&#148; It didn&#146;t work. I kept at it though, and about a year later I finally convinced him to come down and take a look at id. I was working on Quake.</P>
<P>Going from Doom to Quake was a tremendous step. I knew where I wanted to end up, but I wasn&#146;t at all clear what the steps were to get there. I was trying a huge number of approaches, and even the failures were teaching me a lot. My enthusiasm must have been contagious, because he took the job.</P>
<P>Much heroic programming ensued. Several hundred thousand lines of code were written. And rewritten. And rewritten. And rewritten.</P>
<P>In hindsight, I have plenty of regrets about various aspects of Quake, but it is a rare person that doesn&#146;t freely acknowledge the technical triumph of it. We nailed it. Sure, a year from now I will have probably found a new perspective that will make me cringe at the clunkiness of some part of Quake, but at the moment it still looks pretty damn good to me.</P>
<P>I was very happy to have Michael describe much of the Quake technology in his ongoing magazine articles. We learned a lot, and I hope we managed to teach a bit.</P>
<P>When a non-programmer hears about Michael&#146;s articles or the source code I have released, I usually get a stunned &#147;WTF would you do that for???&#148; look.</P>
<P>They don&#146;t get it.</P>
<P>Programming is not a zero-sum game. Teaching something to a fellow programmer doesn&#146;t take it away from you. I&#146;m happy to share what I can, because I&#146;m in it for the love of programming. The Ferraris are just gravy, honest!</P>
<P>This book contains many of the original articles that helped launch my programming career. I hope my contribution to the contents of the later articles can provide similar stepping stones for others.</P>
<P>&#151;<I>John Carmack</I><BR>id Software</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="intro.html">Previous</A></TD>
<TD><A HREF="index.html">Table of Contents</A></TD>
<TD><A HREF="about_author.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

65
about_author.html Normal file
View file

@ -0,0 +1,65 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: About the Author</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=Acknowledgments//-->
<!--PAGES=006//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="about.html">Previous</A></TD>
<TD><A HREF="index.html">Table of Contents</A></TD>
<TD><A HREF="ch01/01-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H2><A NAME="Heading1"></A><FONT COLOR="#000077">Acknowledgments</FONT></H2>
<P>There are many people to thank&#151;because this book was written over many years, in many different settings, an unusually large number of people have played a part in making this book possible. Thanks to Dan Illowsky for not only contributing ideas and encouragement, but also getting me started writing articles long ago, when I lacked the confidence to do it on my own&#151;and for teaching me how to handle the business end of things. Thanks to Will Fastie for giving me my first crack at writing for a large audience in the long-gone but still-missed <I>PC Tech Journal</I>, and for showing me how much fun it could be in his even longer-vanished but genuinely terrific column in <I>Creative Computing</I> (the most enjoyable single column I have ever read in a computer magazine; I used to haunt the mailbox around the beginning of the month just to see what Will had to say). Thanks to Robert Keller, Erin O&#146;Connor, Liz Oakley, Steve Baker, and the rest of the cast of thousands that made <I>Programmer&#146;s Journal</I> a uniquely fun magazine&#151;especially Erin, who did more than anyone to teach me the proper use of the English language. (To this day, Erin will still patiently explain to me when one should use &#147;that&#148; and when one should use &#147;which,&#148; even though eight years of instruction on this and related topics have left no discernible imprint on my brain.) Thanks to Tami Zemel, Monica Berg, and the rest of the <I>Dr. Dobb&#146;s Journal</I> crew for excellent, professional editing, and for just being great people. Thanks to the Coriolis gang for their tireless hard work: Jeff Duntemann, Kim Eoff, Jody Kent, Robert Clarfield, and Anthony Stock. Thanks to Jack Tseng for teaching me a lot about graphics hardware, and even more about how much difference hard work can make. Thanks to John Cockerham, David Stafford, Terje Mathisen, the BitMan, Chris Hecker, Jim Mackraz, Melvin Lafitte, John Navas, Phil Coleman, Anton Truenfels, John Carmack, John Miles, John Bridges, Jim Kent, Hal Hardenbergh, Dave Miller, Steve Levy, Jack Davis, Duane Strong, Daev Rohr, Bill Weber, Dan Gochnauer, Patrick Milligan, Tom Wilson, Peter Klerings, Dave Methvin, Mick Brown, the people in the ibm.pc/fast.code topic on Bix, and all the rest of you who have been so generous with your ideas and suggestions. I&#146;ve done my best to acknowledge contributors by name in this book, but if your name is omitted, my apologies, and consider yourself thanked; this book could not have happened without you. And, of course, thanks to Shay and Emily for their generous patience with my passion for writing and computers.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="about.html">Previous</A></TD>
<TD><A HREF="index.html">Table of Contents</A></TD>
<TD><A HREF="ch01/01-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

76
appendix-a.html Normal file
View file

@ -0,0 +1,76 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--APPENDIX=A//-->
<!--PAGES=1297-1298//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="ch70/70-09.html">Previous</A></TD>
<TD><A HREF="index.html">Table of Contents</A></TD>
<TD><A HREF="book-index.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H2><A NAME="Heading1"></A><FONT COLOR="#000077">Afterword</FONT></H2>
<P>If you&#146;ve followed me this far, you might agree that we&#146;ve come through some rough country. Still, I&#146;m of the opinion that hard-won knowledge is the best knowledge, not only because it sticks to you better, but also because winning a hard race makes it easier to win the next one.
</P>
<P>This is an unusual book in that sense: In addition to being a compilation of much of what I know about fast computer graphics, it is a journal recording some of the process by which I discovered and refined that knowledge. I didn&#146;t just sit down one day to write this book&#151;I wrote it over a period of years and published its component parts in many places. It is a journal of my successes and frustrations, with side glances of my life as it happened along the way.</P>
<P>And there is yet another remarkable thing about this book: You, the reader, helped me write it. Perhaps not you personally, but many people who have read my articles and columns over the years sent me notes asking me questions, suggesting improvements (occasionally by daring me to beat them at the code performance game!) or sometimes just dumping remarkable code into my lap. Where it seemed appropriate, I dropped in the code and sometimes even the words of my correspondents, and the book is much the richer for it.</P>
<P>Here and there, I learned things that had nothing at all to do with fast graphics.</P>
<P>For example: I&#146;m not a doomsayer who thinks American education lags hopelessly behind the rest of the Western world, but now and then something happens that makes me wonder. Some time back, I received a letter from one Melvyn J. Lafitte requesting that I spend some time in my columns describing fast 3-D animation techniques. Melvyn hoped that I would be so kind as to discuss, among other things, hidden surface removal and perspective projection, performed in real time, of course, and preferably in Mode X. Sound familiar?</P>
<P>Melvyn shared with me a hidden surface approach that he had developed. His technique involved defining polygon vertices in clockwise order, as viewed from the visible side. Then, he explained, one can use the cross-product equations found in any math book to determine which way the perpendicular to the polygon is pointing. Better yet, he pointed out, it&#146;s necessary to calculate only the Z component of the perpendicular, and only the sign of the Z component need actually be tested.</P>
<P>What Melvyn described is, of course, backface removal, a key hidden-surface technique that I used heavily in X-Sharp. In general, other hidden surface techniques must be used in conjunction with backface removal, but backface removal is nonetheless important and highly efficient. Simply put, Melvyn had devised for himself one of the fundamental techniques of 3-D drawing.</P>
<P>Melvyn lives in Moens, France. At the time he wrote me, Melvyn was 17 years old. Try to imagine any American 17-year-old of your acquaintance inventing backface removal. Try to imagine any teenager you know even using the phrase &#147;the cross-product equations found in any math book.&#148; Not to mention that Melvyn was able to write a highly technical letter in English; and if Melvyn&#146;s English was something less than flawless, it was perfectly understandable, and, in my experience, vastly better than an average, or even well-educated, American&#146;s French. Please understand, I believe we Americans excel in a wide variety of ways, but I worry that when it comes to math and foreign languages, we are becoming a nation of <I>t&#234;tes de pomme de terre</I>.</P>
<P>Maybe I worry too much. If the glass is half empty, well, it&#146;s also half full. Plainly, something I wrote inspired Melvyn to do something that is wonderful, whether he realizes it or not. And it has been tremendously gratifying to sense in the letters I have received the same feeling of remarkably smart people going out there and doing amazing things just for the sheer unadulterated fun of it.</P>
<P>I don&#146;t think I&#146;m exaggerating too much (well, maybe a little) when I say that this sort of fun is what I live for. I&#146;m glad to see that so many of you share that same passion.</P>
<P>Good luck. Thank you for your input, your code, and all your kind words. Don&#146;t be afraid to attempt the impossible. Simply knowing what is impossible is useful knowledge&#151;and you may well find, in the wake of some unexpected success, that not half of the things we call impossible have any right at all to wear the label.</P>
<P>&#151;Michael Abrash</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="ch70/70-09.html">Previous</A></TD>
<TD><A HREF="index.html">Table of Contents</A></TD>
<TD><A HREF="book-index.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

4600
book-index.html Normal file

File diff suppressed because it is too large Load diff

89
ch01/01-01.html Normal file
View file

@ -0,0 +1,89 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: The Best Optimizer Is between Your Ears</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=01//-->
<!--PAGES=004-007//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../about_author.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="01-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H2 ALIGN="CENTER"><FONT COLOR="#000077"><I>Part I
</I></FONT></H2>
<H2><A NAME="Heading1"></A><FONT COLOR="#000077">Chapter 1<BR>The Best Optimizer Is between Your Ears
</FONT></H2>
<H3><A NAME="Heading2"></A><FONT COLOR="#000077">The Human Element of Code Optimization</FONT></H3>
<P>This book is devoted to a topic near and dear to my heart: writing software that pushes PCs to the limit. Given run-of-the-mill software, PCs run like the 97-pound-weakling minicomputers they are. Give them the proper care, however, and those ugly boxes are capable of miracles. The key is this: Only on microcomputers do you have the run of the whole machine, without layers of operating systems, drivers, and the like getting in the way. You can do <I>anything</I> you want, and you can understand everything that&#146;s going on, if you so wish.</P>
<P>As we&#146;ll see shortly, you should indeed so wish.</P>
<P>Is performance still an issue in this era of cheap 486 computers and super-fast Pentium computers? You bet. How many programs that <I>you</I> use really run so fast that you wouldn&#146;t be happier if they ran faster? We&#146;re so used to slow software that when a compile-and-link sequence that took two minutes on a PC takes just ten seconds on a 486 computer, we&#146;re ecstatic&#151;when in truth we should be settling for nothing less than instantaneous response.</P>
<P>Impossible, you say? Not with the proper design, including incremental compilation and linking, use of extended and/or expanded memory, and well-crafted code. PCs can do just about anything you can imagine (with a few obvious exceptions, such as applications involving super-computer-class number-crunching) if you believe that it can be done, if you understand the computer inside and out, and if you&#146;re willing to think past the obvious solution to unconventional but potentially more fruitful approaches.</P>
<P>My point is simply this: PCs can work wonders. It&#146;s not easy coaxing them into doing that, but it&#146;s rewarding&#151;and it&#146;s sure as heck fun. In this book, we&#146;re going to work some of those wonders, starting...</P>
<P>...now.</P>
<H3><A NAME="Heading3"></A><FONT COLOR="#000077">Understanding High Performance</FONT></H3>
<P>Before we can create high-performance code, we must understand what high performance is. The objective (not always attained) in creating high-performance software is to make the software able to carry out its appointed tasks so rapidly that it responds instantaneously, as far as the user is concerned. In other words, high-performance code should ideally run so fast that any further improvement in the code would be pointless.
</P>
<P>Notice that the above definition most emphatically does <I>not</I> say anything about making the software as fast as possible. It also does not say anything about using assembly language, or an optimizing compiler, or, for that matter, a compiler at all. It also doesn&#146;t say anything about how the code was designed and written. What it does say is that high-performance code shouldn&#146;t get in the user&#146;s way&#151;and that&#146;s <I>all</I>.</P>
<P>That&#146;s an important distinction, because all too many programmers think that assembly language, or the right compiler, or a particular high-level language, or a certain design approach is the answer to creating high-performance code. They&#146;re not, any more than choosing a certain set of tools is the key to building a house. You do indeed need tools to build a house, but any of many sets of tools will do. You also need a blueprint, an understanding of everything that goes into a house, and the ability to <I>use</I> the tools.</P>
<P>Likewise, high-performance programming requires a clear understanding of the purpose of the software being built, an overall program design, algorithms for implementing particular tasks, an understanding of what the computer can do and of what all relevant software is doing&#151;<I>and</I> solid programming skills, preferably using an optimizing compiler or assembly language. The optimization at the end is just the finishing touch, however.</P>
<TABLE WIDTH="100%"><TD WIDTH="5%" VALIGN="TOP"><IMG SRC="images/01-01i.jpg"><TD WIDTH="95%"><SMALL><I>Without good design, good algorithms, and complete understanding of the program&#146;s operation, your carefully optimized code will amount to one of mankind&#146;s least fruitful creations&#151;a fast slow program</I>.</SMALL>
</TABLE>
<P>&#147;What&#146;s a fast slow program?&#148; you ask. That&#146;s a good question, and a brief (true) story is perhaps the best answer.
</P>
<H4 ALIGN="LEFT"><A NAME="Heading4"></A><FONT COLOR="#000077">When Fast Isn&#146;t Fast</FONT></H4>
<P>In the early 1970s, as the first hand-held calculators were hitting the market, I knew a fellow named Irwin. He was a good student, and was planning to be an engineer. Being an engineer back then meant knowing how to use a slide rule, and Irwin could jockey a slipstick with the best of them. In fact, he was so good that he challenged a fellow with a calculator to a duel&#151;and won, becoming a local legend in the process.
</P>
<P>When you get right down to it, though, Irwin was spitting into the wind. In a few short years his hard-earned slipstick skills would be worthless, and the entire discipline would be essentially wiped from the face of the earth. What&#146;s more, anyone with half a brain could see that changeover coming. Irwin had basically wasted the considerable effort and time he had spent optimizing his soon-to-be-obsolete skills.</P>
<P>What does all this have to do with programming? Plenty. When you spend time optimizing poorly-designed assembly code, or when you count on an optimizing compiler to make your code fast, you&#146;re wasting the optimization, much as Irwin did. Particularly in assembly, you&#146;ll find that without proper up-front design and everything else that goes into high-performance design, you&#146;ll waste considerable effort and time on making an inherently slow program as fast as possible&#151;which is still slow&#151;when you could easily have improved performance a great deal more with just a little thought. As we&#146;ll see, handcrafted assembly language and optimizing compilers matter, but less than you might think, in the grand scheme of things&#151;and they scarcely matter at all unless they&#146;re used in the context of a good design and a thorough understanding of both the task at hand and the PC.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../about_author.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="01-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

140
ch01/01-02.html Normal file
View file

@ -0,0 +1,140 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: The Best Optimizer Is between Your Ears</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=01//-->
<!--PAGES=007-009//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="01-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="01-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H3><A NAME="Heading5"></A><FONT COLOR="#000077">Rules for Building High-Performance Code</FONT></H3>
<P>We&#146;ve got the following rules for creating high-performance software:
</P>
<DL>
<DD><B>&#149;</B>&nbsp;&nbsp;Know where you&#146;re going (understand the objective of the software).
<DD><B>&#149;</B>&nbsp;&nbsp;Make a big map (have an overall program design firmly in mind, so the various parts of the program and the data structures work well together).
<DD><B>&#149;</B>&nbsp;&nbsp;Make lots of little maps (design an algorithm for each separate part of the overall design).
<DD><B>&#149;</B>&nbsp;&nbsp;Know the territory (understand exactly how the computer carries out each task).
<DD><B>&#149;</B>&nbsp;&nbsp;Know when it matters (identify the portions of your programs where performance matters, and don&#146;t waste your time optimizing the rest).
<DD><B>&#149;</B>&nbsp;&nbsp;Always consider the alternatives (don&#146;t get stuck on a single approach; odds are there&#146;s a better way, if you&#146;re clever and inventive enough).
<DD><B>&#149;</B>&nbsp;&nbsp;Know how to turn on the juice (optimize the code as best you know how when it <I>does</I> matter).
</DL>
<P>Making rules is easy; the hard part is figuring out how to apply them in the real world. For my money, examining some actual working code is always a good way to get a handle on programming concepts, so let&#146;s look at some of the performance rules in action.
</P>
<H4 ALIGN="LEFT"><A NAME="Heading6"></A><FONT COLOR="#000077">Know Where You&#146;re Going</FONT></H4>
<P>If we&#146;re going to create high-performance code, first we have to know what that code is going to do. As an example, let&#146;s write a program that generates a 16-bit checksum of the bytes in a file. In other words, the program will add each byte in a specified file in turn into a 16-bit value. This checksum value might be used to make sure that a file hasn&#146;t been corrupted, as might occur during transmission over a modem or if a Trojan horse virus rears its ugly head. We&#146;re not going to do anything with the checksum value other than print it out, however; right now we&#146;re only interested in generating that checksum value as rapidly as possible.
</P>
<H4 ALIGN="LEFT"><A NAME="Heading7"></A><FONT COLOR="#000077">Make a Big Map</FONT></H4>
<P>How are we going to generate a checksum value for a specified file? The logical approach is to get the file name, open the file, read the bytes out of the file, add them together, and print the result. Most of those actions are straightforward; the only tricky part lies in reading the bytes and adding them together.
</P>
<H4 ALIGN="LEFT"><A NAME="Heading8"></A><FONT COLOR="#000077">Make Lots of Little Maps</FONT></H4>
<P>Actually, we&#146;re only going to make one little map, because we only have one program section that requires much thought&#151;the section that reads the bytes and adds them up. What&#146;s the best way to do this?
</P>
<P>It would be convenient to load the entire file into memory and then sum the bytes in one loop. Unfortunately, there&#146;s no guarantee that any particular file will fit in the available memory; in fact, it&#146;s a sure thing that many files <I>won&#146;t</I> fit into memory, so that approach is out.</P>
<P>Well, if the whole file won&#146;t fit into memory, one byte surely will. If we read the file one byte at a time, adding each byte to the checksum value before reading the next byte, we&#146;ll minimize memory requirements and be able to handle any size file at all.</P>
<P>Sounds good, eh? Listing 1.1 shows an implementation of this approach. Listing 1.1 uses C&#146;s <B>read()</B> function to read a single byte, adds the byte into the checksum value, and loops back to handle the next byte until the end of the file is reached. The code is compact, easy to write, and functions perfectly&#151;with one slight hitch:</P>
<P>It&#146;s <I>slow</I>.</P>
<P><B>LISTING 1.1 L1-1.C</B></P>
<!-- CODE //-->
<PRE>
/*
* Program to calculate the 16-bit checksum of all bytes in the
* specified file. Obtains the bytes one at a time via read(),
* letting DOS perform all data buffering.
*/
#include &lt;stdio.h&gt;
#include &lt;fcntl.h&gt;
main(int argc, char *argv[]) {
int Handle;
unsigned char Byte;
unsigned int Checksum;
int ReadLength;
if ( argc != 2 ) {
printf(&#147;usage: checksum filename\n&#148;);
exit(1);
}
if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) {
printf(&#147;Can&#146;t open file: %s\n&#148;, argv[1]);
exit(1);
}
/* Initialize the checksum accumulator */
Checksum = 0;
/* Add each byte in turn into the checksum accumulator */
while ( (ReadLength = read(Handle, &Byte, sizeof(Byte))) &gt; 0 ) {
Checksum &#43;= (unsigned int) Byte;
}
if ( ReadLength == -1 ) {
printf(&#147;Error reading file %s\n&#148;, argv[1]);
exit(1);
}
/* Report the result */
printf(&#147;The checksum is: %u\n&#148;, Checksum);
exit(0);
}
</PRE>
<!-- END CODE //-->
<P>Table 1.1 shows the time taken for Listing 1.1 to generate a checksum of the WordPerfect version 4.2 thesaurus file, TH.WP (362,293 bytes in size), on a 10 MHz AT machine of no special parentage. Execution times are given for Listing 1.1 compiled with Borland and Microsoft compilers, with optimization both on and off; all four times are pretty much the same, however, and all are much too slow to be acceptable. Listing 1.1 requires over two and one-half minutes to checksum <I>one</I> file!</P>
<TABLE WIDTH="100%"><TD WIDTH="5%" VALIGN="TOP"><IMG SRC="images/01-02i.jpg"><TD WIDTH="95%"><SMALL><I>Listings 1.2 and 1.3 form the C/assembly equivalent to Listing 1.1, and Listings 1.6 and 1.7 form the C/assembly equivalent to Listing 1.5.</I></SMALL>
</TABLE>
<P>These results make it clear that it&#146;s folly to rely on your compiler&#146;s optimization to make your programs fast. Listing 1.1 is simply poorly designed, and no amount of compiler optimization will compensate for that failing. To drive home the point, conListings 1.2 and 1.3, which together are equivalent to Listing 1.1 except that the entire checksum loop is written in tight assembly code. The assembly language implementation is indeed faster than any of the C versions, as shown in Table 1.1, but it&#146;s less than 10 percent faster, and it&#146;s still unacceptably slow.
</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="01-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="01-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

238
ch01/01-03.html Normal file
View file

@ -0,0 +1,238 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: The Best Optimizer Is between Your Ears</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=01//-->
<!--PAGES=010-013//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="01-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="01-04.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<TABLE WIDTH="100%">
<TR>
<TD COLSPAN="7"><HR>
<TR>
<TH WIDTH="15%" ALIGN="LEFT" VALIGN="BOTTOM">Listing
<TH WIDTH="15%" ALIGN="LEFT" VALIGN="BOTTOM">Borland
<TH WIDTH="12%" ALIGN="LEFT" VALIGN="BOTTOM">Microsoft
<TH WIDTH="12%" ALIGN="LEFT" VALIGN="BOTTOM">Borland
<TH WIDTH="12%" ALIGN="LEFT" VALIGN="BOTTOM">Microsoft
<TH WIDTH="12%" ALIGN="LEFT" VALIGN="BOTTOM">Assembly
<TH WIDTH="12%" ALIGN="LEFT" VALIGN="BOTTOM">Optimization<BR>Ratio
<TR>
<TD COLSPAN="7"><HR>
<TR>
<TD VALIGN="BOTTOM" ALIGN="LEFT">
<TD VALIGN="BOTTOM" ALIGN="LEFT">(no opt)
<TD VALIGN="BOTTOM" ALIGN="LEFT">(no opt)
<TD VALIGN="BOTTOM" ALIGN="LEFT">(opt)
<TD VALIGN="BOTTOM" ALIGN="LEFT">(opt)
<TR>
<TD VALIGN="BOTTOM" ALIGN="LEFT">1
<TD VALIGN="BOTTOM" ALIGN="LEFT">166.9
<TD VALIGN="BOTTOM" ALIGN="LEFT">166.8
<TD VALIGN="BOTTOM" ALIGN="LEFT">167.0
<TD VALIGN="BOTTOM" ALIGN="LEFT">165.8
<TD VALIGN="BOTTOM" ALIGN="LEFT">155.1
<TD VALIGN="BOTTOM" ALIGN="LEFT">1.08
<TR>
<TD VALIGN="BOTTOM" ALIGN="LEFT">4
<TD VALIGN="BOTTOM" ALIGN="LEFT">13.5
<TD VALIGN="BOTTOM" ALIGN="LEFT">13.6
<TD VALIGN="BOTTOM" ALIGN="LEFT">13.5
<TD VALIGN="BOTTOM" ALIGN="LEFT">13.5
<TD VALIGN="BOTTOM" ALIGN="LEFT">...
<TD VALIGN="BOTTOM" ALIGN="LEFT">1.01
<TR>
<TD VALIGN="BOTTOM" ALIGN="LEFT">5
<TD VALIGN="BOTTOM" ALIGN="LEFT">4.7
<TD VALIGN="BOTTOM" ALIGN="LEFT">5.5
<TD VALIGN="BOTTOM" ALIGN="LEFT">3.8
<TD VALIGN="BOTTOM" ALIGN="LEFT">3.4
<TD VALIGN="BOTTOM" ALIGN="LEFT">2.7
<TD VALIGN="BOTTOM" ALIGN="LEFT">2.04
<TR>
<TD VALIGN="BOTTOM" ALIGN="LEFT">Ratio best<BR>designed<BR>to worst<BR>designed
<TD VALIGN="BOTTOM" ALIGN="LEFT">35.51
<TD VALIGN="BOTTOM" ALIGN="LEFT">30.33
<TD VALIGN="BOTTOM" ALIGN="LEFT">43.95
<TD VALIGN="BOTTOM" ALIGN="LEFT">48.76
<TD VALIGN="BOTTOM" ALIGN="LEFT">57.44
<TR>
<TD COLSPAN="7"><SMALL><B>Note:</B> The execution times (in seconds) for this chapter&#146;s listings were timed when the compiled listings were run on the WordPerfect 4.2 thesaurus file TH.WP (362,293 bytes in size), as compiled in the small model with Borland and Microsoft compilers with optimization on (opt) and off (no opt). All times were measured with Paradigm Systems&#146; TIMER program on a 10 MHz 1-wait-state AT clone with a 28-ms hard disk, with disk caching turned off.</SMALL>
<TR>
<TD COLSPAN="7"><HR>
<TR>
<TH ALIGN="LEFT" COLSPAN="7">Table 1.1 Execution Times for WordPerfect Checksum.
<TR>
<TD COLSPAN="7"><HR>
<TR>
</TABLE>
<P><B>LISTING 1.2 L1-2.C</B></P>
<!-- CODE //-->
<PRE>
/*
* Program to calculate the 16-bit checksum of the stream of bytes
* from the specified file. Obtains the bytes one at a time in
* assembler, via direct calls to DOS.
*/
#include &lt;stdio.h&gt;
#include &lt;fcntl.h&gt;
main(int argc, char *argv[]) {
int Handle;
unsigned char Byte;
unsigned int Checksum;
int ReadLength;
if ( argc != 2 ) {
printf(&#147;usage: checksum filename\n&#148;);
exit(1);
}
if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) {
printf(&#147;Can&#146;t open file: %s\n&#148;, argv[1]);
exit(1);
}
if ( !ChecksumFile(Handle, &Checksum) ) {
printf(&#147;Error reading file %s\n&#148;, argv[1]);
exit(1);
}
/* Report the result */
printf(&#147;The checksum is: %u\n&#148;, Checksum);
exit(0);
}
</PRE>
<!-- END CODE //-->
<P><B>LISTING 1.3 L1-3.ASM</B></P>
<!-- CODE //-->
<PRE>
; Assembler subroutine to perform a 16-bit checksum on the file
; opened on the passed-in handle. Stores the result in the
; passed-in checksum variable. Returns 1 for success, 0 for error.
;
; Call as:
; int ChecksumFile(unsigned int Handle, unsigned int *Checksum);
;
; where:
; Handle = handle # under which file to checksum is open
; Checksum = pointer to unsigned int variable checksum is
; to be stored in
;
; Parameter structure:
;
Parms struc
dw ? ;pushed BP
dw ? ;return address
Handle dw ?
Checksum dw ?
Parms ends
;
.model small
.data
TempWord label word
TempByte db ? ;each byte read by DOS will be stored here
db 0 ;high byte of TempWord is always 0
;for 16-bit adds
;
.code
public _ChecksumFile
_ChecksumFile proc near
push bp
mov bp,sp
push si ;save C&#146;s register variable
;
mov bx,[bp&#43;Handle] ;get file handle
sub si,si ;zero the checksum ;accumulator
mov cx,1 ;request one byte on each ;read
mov dx,offset TempByte ;point DX to the byte in
;which DOS should store
;each byte read
ChecksumLoop:
mov ah,3fh ;DOS read file function #
int 21h ;read the byte
jcErrorEnd;an error occurred
and ax,ax ;any bytes read?
jz Success ;no-end of file reached-we&#146;re done
add si,[TempWord] ;add the byte into the
;checksum total
jmpChecksumLoop
ErrorEnd:
sub ax,ax ;error
jmp short Done
Success:
mov bx,[bp&#43;Checksum] ;point to the checksum variable
mov [bx],si ;save the new checksum
mov ax,1 ;success
;
Done:
pop si ;restore C&#146;s register variable
pop bp
ret
_ChecksumFileendp
end
</PRE>
<!-- END CODE //-->
<P>The lesson is clear: Optimization makes code faster, but without proper design, optimization just creates fast slow code.
</P>
<P>Well, then, how are we going to improve our design? Before we can do that, we have to understand what&#146;s wrong with the current design.</P>
<H4 ALIGN="LEFT"><A NAME="Heading9"></A><FONT COLOR="#000077">Know the Territory</FONT></H4>
<P>Just why is Listing 1.1 so slow? In a word: overhead. The C library implements the <B>read()</B> function by calling DOS to read the desired number of bytes. (I figured this out by watching the code execute with a debugger, but you can buy library source code from both Microsoft and Borland.) That means that Listing 1.1 (and Listing 1.3 as well) executes one DOS function per byte processed&#151;and DOS functions, especially this one, come with a lot of overhead.</P>
<P>For starters, DOS functions are invoked with interrupts, and interrupts are among the slowest instructions of the x86 family CPUs. Then, DOS has to set up internally and branch to the desired function, expending more cycles in the process. Finally, DOS has to search its own buffers to see if the desired byte has already been read, read it from the disk if not, store the byte in the specified location, and return. All of that takes a <I>long</I> time&#151;far, far longer than the rest of the main loop in Listing 1.1. In short, Listing 1.1 spends virtually all of its time executing <B>read(),</B> and most of that time is spent somewhere down in DOS.</P>
<P>You can verify this for yourself by watching the code with a debugger or using a code profiler, but take my word for it: There&#146;s a great deal of overhead to DOS calls, and that&#146;s what&#146;s draining the life out of Listing 1.1.</P>
<P>How can we speed up Listing 1.1? It should be clear that we must somehow avoid invoking DOS for every byte in the file, and that means reading more than one byte at a time, then buffering the data and parceling it out for examination one byte at a time. By gosh, that&#146;s a description of C&#146;s stream I/O feature, whereby C reads files in chunks and buffers the bytes internally, doling them out to the application as needed by reading them from memory rather than calling DOS. Let&#146;s try using stream I/O and see what happens.</P>
<P>Listing 1.4 is similar to Listing 1.1, but uses <B>fopen()</B> and <B>getc()</B> (rather than <B>open()</B> and <B>read()</B>) to access the file being checksummed. The results confirm our theories splendidly, and validate our new design. As shown in Table 1.1, Listing 1.4 runs more than an order of magnitude faster than even the assembly version of Listing 1.1, <I>even though Listing 1.1 and Listing 1.4 look almost the same</I>. To the casual observer, <B>read()</B> and <B>getc()</B> would seem slightly different but pretty much interchangeable, and yet in this application the performance difference between the two is about the same as that between a 4.77 MHz PC and a 16 MHz 386.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="01-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="01-04.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

124
ch01/01-04.html Normal file
View file

@ -0,0 +1,124 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: The Best Optimizer Is between Your Ears</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=01//-->
<!--PAGES=013-015//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="01-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="01-05.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<TABLE WIDTH="100%">
<TD WIDTH="5%" VALIGN="TOP"><IMG SRC="images/01-03i.jpg"><TD WIDTH="95%"><SMALL><I>Make sure you understand what really goes on when you insert a seemingly-innocuous function call into the time-critical portions of your code.</I></SMALL>
</TABLE>
<P>In this case that means knowing how DOS and the C/C<SMALL>&#43;&#43;</SMALL> file-access libraries do their work. In other words, <I>know the territory</I>!</P>
<P><B>LISTING 1.4 L1-4.C</B></P>
<!-- CODE //-->
<PRE>
/*
* Program to calculate the 16-bit checksum of the stream of bytes
* from the specified file. Obtains the bytes one at a time via
* getc(), allowing C to perform data buffering.
*/
#include &lt;stdio.h&gt;
main(int argc, char *argv[]) {
FILE *CheckFile;
int Byte;
unsigned int Checksum;
if ( argc != 2 ) {
printf(&#147;usage: checksum filename\n&#148;);
exit(1);
}
if ( (CheckFile = fopen(argv[1], &#147;rb&#148;)) == NULL ) {
printf(&#147;Can&#146;t open file: %s\n&#148;, argv[1]);
exit(1);
}
/* Initialize the checksum accumulator */
Checksum = 0;
/* Add each byte in turn into the checksum accumulator */
while ( (Byte = getc(CheckFile)) != EOF ) {
Checksum &#43;= (unsigned int) Byte;
}
/* Report the result */
printf(&#147;The checksum is: %u\n&#148;, Checksum);
exit(0);
}
</PRE>
<!-- END CODE //-->
<H4 ALIGN="LEFT"><A NAME="Heading10"></A><FONT COLOR="#000077">Know When It Matters</FONT></H4>
<P>The last section contained a particularly interesting phrase: <I>the time-critical portions of your code</I>. Time-critical portions of your code are those portions in which the speed of the code makes a significant difference in the overall performance of your program&#151;and by &#147;significant,&#148; I don&#146;t mean that it makes the code 100 percent faster, or 200 percent, or any particular amount at all, but rather that it makes the program more responsive and/or usable <I>from the user&#146;s perspective</I>.</P>
<P>Don&#146;t waste time optimizing non-time-critical code: set-up code, initialization code, and the like. Spend your time improving the performance of the code inside heavily-used loops and in the portions of your programs that directly affect response time. Notice, for example, that I haven&#146;t bothered to implement a version of the checksum program entirely in assembly; Listings 1.2 and 1.6 call assembly subroutines that handle the time-critical operations, but C is still used for checking command-line parameters, operning files, printing, and the like.</P>
<TABLE WIDTH="100%"><TD WIDTH="5%" VALIGN="TOP"><IMG SRC="images/01-04i.jpg"><TD WIDTH="95%"><SMALL><I>If you were to implement any of the listings in this chapter entirely in hand-optimized assembly, I suppose you might get a performance improvement of a few percent&#151;but I rather doubt you&#146;d get even that much, and you&#146;d sure as heck spend an awful lot of time for whatever meager improvement does result. Let C do what it does well, and use assembly only when it makes a perceptible difference.</I></SMALL>
</TABLE>
<P>Besides, we don&#146;t want to optimize until the design is refined to our satisfaction, and that won&#146;t be the case until we&#146;ve thought about other approaches.
</P>
<H4 ALIGN="LEFT"><A NAME="Heading11"></A><FONT COLOR="#000077">Always Consider the Alternatives</FONT></H4>
<P>Listing 1.4 is good, but let&#146;s see if there are other&#151;perhaps less obvious&#151;ways to get the same results faster. Let&#146;s start by considering why Listing 1.4 is so much better than Listing 1.1. Like <B>read()</B>, <B>getc()</B> calls DOS to read from the file; the speed improvement of Listing 1.4 over Listing 1.1 occurs because <B>getc()</B> eads many bytes at once via DOS, then manages those bytes for us. That&#146;s faster than reading them one at a time using <B>read()</B>&#151;but there&#146;s no reason to think that it&#146;s faster than having our program read and manage blocks itself. Easier, yes, but not faster.</P>
<P>Consider this: Every invocation of <B>getc()</B> involves pushing a parameter, executing a call to the C library function, getting the parameter (in the C library code), looking up information about the desired stream, unbuffering the next byte from the stream, and returning to the calling code. That takes a considerable amount of time, especially by contrast with simply maintaining a pointer to a buffer and whizzing through the data in the buffer inside a single loop.</P>
<P>There are four reasons that many programmers would give for not trying to improve on Listing 1.4:</P>
<DL>
<DD><B>1.</B>&nbsp;&nbsp;The code is already fast enough.
<DD><B>2.</B>&nbsp;&nbsp;The code works, and some people are content with code that works, even when it&#146;s slow enough to be annoying.
<DD><B>3.</B>&nbsp;&nbsp;The C library is written in optimized assembly, and it&#146;s likely to be faster than any code that the average programmer could write to perform essentially the same function.
<DD><B>4.</B>&nbsp;&nbsp;The C library conveniently handles the buffering of file data, and it would be a nuisance to have to implement that capability.
</DL>
<P>I&#146;ll ignore the first reason, both because performance is no longer an issue if the code is fast enough and because the current application does <I>not</I> run fast enough&#151;13 seconds is a long time. (Stop and wait for 13 seconds while you&#146;re doing something intense, and you&#146;ll see just how long it is.)</P>
<P>The second reason is the hallmark of the mediocre programmer. Know when optimization matters&#151;and then optimize when it does!</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="01-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="01-05.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

192
ch01/01-05.html Normal file
View file

@ -0,0 +1,192 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: The Best Optimizer Is between Your Ears</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=01//-->
<!--PAGES=015-017//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="01-04.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="01-06.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>The third reason is often fallacious. C library functions are not always written in assembly, nor are they always particularly well-optimized. (In fact, they&#146;re often written for <I>portability</I>, which has nothing to do with optimization.) What&#146;s more, they&#146;re general-purpose functions, and often can be outperformed by well-but-not- brilliantly-written code that is well-matched to a specific task. As an example, consider Listing 1.5, which uses internal buffering to handle blocks of bytes at a time. Table 1.1 shows that Listing 1.5 is 2.5 to 4 times faster than Listing 1.4 (and as much as 49 times faster than Listing 1.1!), even though it uses no assembly at all.</P>
<TABLE WIDTH="100%"><TD WIDTH="5%" VALIGN="TOP"><IMG SRC="images/01-05i.jpg"><TD WIDTH="95%"><SMALL><I>Clearly, you can do well by using special-purpose C code in place of a C library function&#151;if you have a thorough understanding of how the C library function operates and exactly what your application needs done. Otherwise, you&#146;ll end up rewriting C library functions in C, which makes no sense at all.</I></SMALL>
</TABLE>
<P><B>LISTING 1.5 L1-5.C</B></P>
<!-- CODE //-->
<PRE>
/*
* Program to calculate the 16-bit checksum of the stream of bytes
* from the specified file. Buffers the bytes internally, rather
* than letting C or DOS do the work.
*/
#include &lt;stdio.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;alloc.h&gt; /* alloc.h for Borland,
malloc.h for Microsoft */
#define BUFFER_SIZE 0x8000 /* 32Kb data buffer */
main(int argc, char *argv[]) {
int Handle;
unsigned int Checksum;
unsigned char *WorkingBuffer, *WorkingPtr;
int WorkingLength, LengthCount;
if ( argc != 2 ) {
printf(&#147;usage: checksum filename\n&#148;);
exit(1);
}
if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) {
printf(&#147;Can&#146;t open file: %s\n&#148;, argv[1]);
exit(1);
}
/* Get memory in which to buffer the data */
if ( (WorkingBuffer = malloc(BUFFER_SIZE)) == NULL ) {
printf(&#147;Can&#146;t get enough memory\n&#148;);
exit(1);
}
/* Initialize the checksum accumulator */
Checksum = 0;
/* Process the file in BUFFER_SIZE chunks */
do {
if ( (WorkingLength = read(Handle, WorkingBuffer,
BUFFER_SIZE)) == -1 ) {
printf(&#147;Error reading file %s\n&#148;, argv[1]);
exit(1);
}
/* Checksum this chunk */
WorkingPtr = WorkingBuffer;
LengthCount = WorkingLength;
while ( LengthCount&#150;&#150; ) {
/* Add each byte in turn into the checksum accumulator */
Checksum &#43;= (unsigned int) *WorkingPtr&#43;&#43;;
}
} while ( WorkingLength );
/* Report the result */
printf(&#147;The checksum is: %u\n&#148;, Checksum);
exit(0);
}
</PRE>
<!-- END CODE //-->
<P>That brings us to the fourth reason: avoiding an internal-buffered implementation like Listing 1.5 because of the difficulty of coding such an approach. True, it is easier to let a C library function do the work, but it&#146;s not all that hard to do the buffering internally. The key is the concept of handling data in <I>restartable blocks;</I> that is, reading a chunk of data, operating on the data until it runs out, suspending the operation while more data is read in, and then continuing as though nothing had happened.</P>
<P>In Listing 1.5 the restartable block implementation is pretty simple because checksumming works with one byte at a time, forgetting about each byte immediately after adding it into the total. Listing 1.5 reads in a block of bytes from the file, checksums the bytes in the block, and gets another block, repeating the process until the entire file has been processed. In Chapter 5, we&#146;ll see a more complex restartable block implementation, involving searching for text strings.</P>
<P>At any rate, Listing 1.5 isn&#146;t much more complicated than Listing 1.4&#151;and it&#146;s a <I>lot</I> faster. Always consider the alternatives; a bit of clever thinking and program redesign can go a long way.</P>
<H4 ALIGN="LEFT"><A NAME="Heading12"></A><FONT COLOR="#000077">Know How to Turn On the Juice</FONT></H4>
<P>I have said time and again that optimization is pointless until the design is settled. When that time comes, however, optimization can indeed make a significant difference. Table 1.1 indicates that the optimized version of Listing 1.5 produced by Microsoft C outperforms an unoptimized version of the same code by more than 60 percent. What&#146;s more, a mostly-assembly version of Listing 1.5, shown in Listings 1.6 and 1.7, outperforms even the best-optimized C version of List1.5 by 26 percent. These are considerable improvements, well worth pursuing&#151;once the design has been maxed out.
</P>
<P><B>LISTING 1.6 L1-6.C</B></P>
<!-- CODE //-->
<PRE>
/*
* Program to calculate the 16-bit checksum of the stream of bytes
* from the specified file. Buffers the bytes internally, rather
* than letting C or DOS do the work, with the time-critical
* portion of the code written in optimized assembler.
*/
#include &lt;stdio.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;alloc.h&gt; /* alloc.h for Borland,
malloc.h for Microsoft */
#define BUFFER_SIZE 0x8000 /* 32K data buffer */
main(int argc, char *argv[]) {
int Handle;
unsigned int Checksum;
unsigned char *WorkingBuffer;
int WorkingLength;
if ( argc != 2 ) {
printf(&#147;usage: checksum filename\n&#148;);
exit(1);
}
if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) {
printf(&#147;Can&#146;t open file: %s\n&#148;, argv[1]);
exit(1);
}
/* Get memory in which to buffer the data */
if ( (WorkingBuffer = malloc(BUFFER_SIZE)) == NULL ) {
printf(&#147;Can&#146;t get enough memory\n&#148;);
exit(1);
}
/* Initialize the checksum accumulator */
Checksum = 0;
/* Process the file in 32K chunks */
do {
if ( (WorkingLength = read(Handle, WorkingBuffer,
BUFFER_SIZE)) == -1 ) {
printf(&#147;Error reading file %s\n&#148;, argv[1]);
exit(1);
}
/* Checksum this chunk if there&#146;s anything in it */
if ( WorkingLength )
ChecksumChunk(WorkingBuffer, WorkingLength, &Checksum);
} while ( WorkingLength );
/* Report the result */
printf(&#147;The checksum is: %u\n&#148;, Checksum);
exit(0);
}
</PRE>
<!-- END CODE //-->
<P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="01-04.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="01-06.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

132
ch01/01-06.html Normal file
View file

@ -0,0 +1,132 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: The Best Optimizer Is between Your Ears</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=01//-->
<!--PAGES=018-019//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="01-05.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch02/02-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P><B>LISTING 1.7 L1-7.ASM</B></P>
<!-- CODE //-->
<PRE>
; Assembler subroutine to perform a 16-bit checksum on a block of
; bytes 1 to 64K in size. Adds checksum for block into passed-in
; checksum.
;
; Call as:
; void ChecksumChunk(unsigned char *Buffer,
; unsigned int BufferLength, unsigned int *Checksum);
;
; where:
; Buffer = pointer to start of block of bytes to checksum
; BufferLength = # of bytes to checksum (0 means 64K, not 0)
; Checksum = pointer to unsigned int variable checksum is
;stored in
;
; Parameter structure:
;
Parms struc
dw ? ;pushed BP
dw ? ;return address
Buffer dw ?
BufferLength dw ?
Checksum dw ?
Parmsends
;
.model small
.code
public _ChecksumChunk
_ChecksumChunkprocnear
push bp
mov bp,sp
push si ;save C&#146;s register variable
;
cld ;make LODSB increment SI
mov si,[bp&#43;Buffer] ;point to buffer
mov cx,[bp&#43;BufferLength] ;get buffer length
mov bx,[bp&#43;Checksum] ;point to checksum variable
mov dx,[bx] ;get the current checksum
sub ah,ah ;so AX will be a 16-bit value after LODSB
ChecksumLoop:
lodsb ;get the next byte
add dx,ax ;add it into the checksum total
loop ChecksumLoop ;continue for all bytes in block
mov [bx],dx ;save the new checksum
;
pop si ;restore C&#146;s register variable
pop bp
ret
_ChecksumChunkendp
end
</PRE>
<!-- END CODE //-->
<P>Note that in Table 1.1, optimization makes little difference except in the case of Listing 1.5, where the design has been refined considerably. Execution time in the other cases is dominated by time spent in DOS and/or the C library, so optimization of the code you write is pretty much irrelevant. What&#146;s more, while the approximately two-times improvement we got by optimizing is not to be sneezed at, it pales against the up-to-50-times improvement we got by redesigning.
</P>
<P>By the way, the execution times even of Listings 1.6 and 1.7 are dominated by DOS disk access times. If a disk cache is enabled and the file to be checksummed is already in the cache, the assembly version is three times as fast as the C version. In other words, the inherent nature of this application limits the performance improvement that can be obtained via assembly. In applications that are more CPU-intensive and less disk-bound, particularly those applications in which string instructions and/or unrolled loops can be used effectively, assembly tends to be considerably faster relative to C than it is in this very specific case.</P>
<TABLE WIDTH="100%"><TD WIDTH="5%" VALIGN="TOP"><IMG SRC="images/01-06i.jpg"><TD WIDTH="95%"><SMALL><I>Don&#146;t get hung up on optimizing compilers or assembly language&#151;the best optimizer is between your ears.</I></SMALL>
</TABLE>
<P>All this is basically a way of saying: Know where you&#146;re going, know the territory, and know when it matters.
</P>
<H3><A NAME="Heading13"></A><FONT COLOR="#000077">Where We&#146;ve Been, What We&#146;ve Seen</FONT></H3>
<P>What have we learned? Don&#146;t let other people&#146;s code&#151;even DOS&#151;do the work for you when speed matters, at least not without knowing what that code does and how well it performs.
</P>
<P>Optimization only matters after you&#146;ve done your part on the program design end. Consider the ratios on the vertical axis of Table 1.1, which show that optimization is almost totally wasted in the checksumming application without an efficient design. Optimization is no panacea. Table 1.1 shows a two-times improvement from optimization&#151;and a 50-times-plus improvement from redesign. The longstanding debate about which C compiler optimizes code best doesn&#146;t matter quite so much in light of Table 1.1, does it? Your organic optimizer matters much more than your compiler&#146;s optimizer, and there&#146;s always assembly for those usually small sections of code where performance really matters.</P>
<H4 ALIGN="LEFT"><A NAME="Heading14"></A><FONT COLOR="#000077">Where We&#146;re Going</FONT></H4>
<P>This chapter has presented a quick step-by-step overview of the design process. I&#146;m not claiming that this is the only way to create high-performance code; it&#146;s just an approach that works for me. Create code however you want, but never forget that design matters more than detailed optimization. Never stop looking for inventive ways to boost performance&#151;and never waste time speeding up code that doesn&#146;t need to be sped up.
</P>
<P>I&#146;m going to focus on specific ways to create high-performance code from now on. In Chapter 5, we&#146;ll continue to look at restartable blocks and internal buffering, in the form of a program that searches files for text strings.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="01-05.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch02/02-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

BIN
ch01/images/01-01i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch01/images/01-02i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
ch01/images/01-03i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch01/images/01-04i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch01/images/01-05i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch01/images/01-06i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

109
ch02/02-01.html Normal file
View file

@ -0,0 +1,109 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: A World Apart</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=02//-->
<!--PAGES=021-025//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch01/01-06.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="02-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H2><A NAME="Heading1"></A><FONT COLOR="#000077">Chapter 2<BR>A World Apart
</FONT></H2>
<H3><A NAME="Heading2"></A><FONT COLOR="#000077">The Unique Nature of Assembly Language Optimization</FONT></H3>
<P>As I showed in the previous chapter, optimization is by no means always a matter of &#147;dropping into assembly.&#148; In fact, in performance tuning high-level language code, assembly should be used rarely, and then only after you&#146;ve made sure a badly chosen or clumsily implemented algorithm isn&#146;t eating you alive. Certainly if you use assembly at all, make absolutely sure you use it <I>right</I>. The potential of assembly code to run <I>slowly</I> is poorly understood by a lot of people, but that potential is great, especially in the hands of the ignorant.</P>
<P>Truly great optimization, however, happens <I>only</I> at the assembly level, and it happens in response to a set of dynamics that is totally different from that governing C/C<SMALL>&#43;&#43;</SMALL> or Pascal optimization. I&#146;ll be speaking of assembly-level optimization time and again in this book, but when I do, I think it will be helpful if you have a grasp of those assembly specific dynamics.</P>
<P>As usual, the best way to wade in is to present a real-world example.</P>
<H3><A NAME="Heading3"></A><FONT COLOR="#000077">Instructions: The Individual versus the Collective</FONT></H3>
<P>Some time ago, I was asked to work over a critical assembly subroutine in order to make it run as fast as possible. The task of the subroutine was to construct a nibble out of four bits read from different bytes, rotating and combining the bits so that they ultimately ended up neatly aligned in bits 3-0 of a single byte. (In case you&#146;re curious, the object was to construct a 16-color pixel from bits scattered over 4 bytes.) I examined the subroutine line by line, saving a cycle here and a cycle there, until the code truly seemed to be optimized. When I was done, the key part of the code looked something like this:
</P>
<!-- CODE SNIP //-->
<PRE>
LoopTop:
lodsb ;get the next byte to extract a bit from
and al,ah ;isolate the bit we want
rol al,cl ;rotate the bit into the desired position
or bl,al ;insert the bit into the final nibble
dec cx ;the next bit goes 1 place to the right
dec dx ;count down the number of bits
jnz LoopTop ;process the next bit, if any
</PRE>
<!-- END CODE SNIP //-->
<P>Now, it&#146;s hard to write code that&#146;s much faster than seven instructions, only one of which accesses memory, and most programmers would have called it a day at this point. Still, something bothered me, so I spent a bit of time going over the code again. Suddenly, the answer struck me&#151;the code was rotating each bit into place separately, so that a multibit rotation was being performed every time through the loop, for a total of four separate time-consuming multibit rotations!
</P>
<TABLE WIDTH="100%"><TD WIDTH="5%" VALIGN="TOP"><IMG SRC="images/02-01i.jpg"><TD WIDTH="95%"><SMALL><I>While the instructions themselves were individually optimized, the overall approach did not make the best possible use of the instructions.</I></SMALL>
</TABLE>
<P>I changed the code to the following:
</P>
<!-- CODE //-->
<PRE>
LoopTop:
lodsb ;get the next byte to extract a bit from
and al,ah ;isolate the bit we want
or bl,al ;insert the bit into the final nibble
rol bl,1 ;make room for the next bit
dec dx ;count down the number of bits
jnz LoopTop ;process the next bit, if any
rol bl,cl ;rotate all four bits into their final
; positions at the same time
</PRE>
<!-- END CODE //-->
<P>This moved the costly multibit rotation out of the loop so that it was performed just once, rather than four times. While the code may not look much different from the original, and in fact still contains exactly the same number of instructions, the performance of the entire subroutine improved by about 10 percent from just this one change. (Incidentally, that wasn&#146;t the end of the optimization; I eliminated the <B>DEC</B> and <B>JNJ</B> instructions by expanding the four iterations of the loop&#151;but that&#146;s a tale for another chapter.)</P>
<P>The point is this: To write truly superior assembly programs, you need to know what the various instructions do and which instructions execute fastest...and more. You must also learn to look at your programming problems from a variety of perspectives so that you can put those fast instructions to work in the most effective ways.</P>
<H3><A NAME="Heading4"></A><FONT COLOR="#000077">Assembly Is Fundamentally Different</FONT></H3>
<P>Is it really so hard as all that to write good assembly code for the PC? Yes! Thanks to the decidedly quirky nature of the x86 family CPUs, assembly language differs fundamentally from other languages, and is undeniably harder to work with. On the other hand, the potential of assembly code is much greater than that of other languages, as well.
</P>
<P>To understand why this is so, consider how a program gets written. A programmer examines the requirements of an application, designs a solution at some level of abstraction, and then makes that design come alive in a code implementation. If not handled properly, the transformation that takes place between conception and implementation can reduce performance tremendously; for example, a programmer who implements a routine to search a list of 100,000 sorted items with a linear rather than binary search will end up with a disappointingly slow program.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch01/01-06.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="02-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

86
ch02/02-02.html Normal file
View file

@ -0,0 +1,86 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: A World Apart</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=02//-->
<!--PAGES=025-028//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="02-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="02-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H4 ALIGN="LEFT"><A NAME="Heading5"></A><FONT COLOR="#000077">Transformation Inefficiencies</FONT></H4>
<P>No matter how well an implementation is derived from the corresponding design, however, high-level languages like C/C<SMALL>&#43;&#43;</SMALL> and Pascal inevitably introduce additional transformation inefficiencies, as shown in Figure 2.1.</P>
<P>The process of turning a design into executable code by way of a high-level language involves two transformations: one performed by the programmer to generate source code, and another performed by the compiler to turn source code into machine language instructions. Consequently, the machine language code generated by compilers is usually less than optimal given the requirements of the original design.</P>
<P>High-level languages provide artificial environments that lend themselves relatively well to human programming skills, in order to ease the transition from design to implementation. The price for this ease of implementation is a considerable loss of efficiency in transforming source code into machine language. This is particularly true given that the x86 family in real and 16-bit protected mode, with its specialized memory-addressing instructions and segmented memory architecture, does not lend itself particularly well to compiler design. Even the 32-bit mode of the 386 and its successors, with their more powerful addressing modes, offer fewer registers than compilers would like.</P>
<P><A NAME="Fig1"><!-- </A><A HREF="javascript:displayWindow('images/02-01.jpg',417,202 )"> --><IMG SRC="images/02-01.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/02-01.jpg',417,202)"> --><FONT COLOR="#000077"><B>Figure 2.1</B></FONT></A>&nbsp;&nbsp;<I>The high-level language transformation inefficiencies.</I>
</P>
<P>Assembly, on the other hand, is simply a human-oriented representation of machine language. As a result, assembly provides a difficult programming environment&#151;the bare hardware and systems software of the computer&#151;<I>but properly constructed assembly programs suffer no transformation loss</I>, as shown in Figure 2.2.</P>
<P>Only one transformation is required when creating an assembler program, and that single transformation is completely under the programmer&#146;s control. Assemblers perform no transformation from source code to machine language; instead, they merely map assembler instructions to machine language instructions on a one-to-one basis. As a result, the programmer is able to produce machine language code that&#146;s precisely tailored to the needs of each task a given application requires.</P>
<P><A NAME="Fig2"><!-- </A><A HREF="javascript:displayWindow('images/02-02.jpg',414,201 )"> --><IMG SRC="images/02-02.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/02-02.jpg',414,201)"> --><FONT COLOR="#000077"><B>Figure 2.2</B></FONT></A>&nbsp;&nbsp;<I>Properly constructed assembly programs suffer no transformation loss.</I>
</P>
<P>The key, of course, is the programmer, since in assembly the programmer must essentially perform the transformation from the application specification to machine language entirely on his or her own. (The assembler merely handles the <I>direct</I> translation from assembly to machine language.)</P>
<H4 ALIGN="LEFT"><A NAME="Heading6"></A><FONT COLOR="#000077">Self-Reliance</FONT></H4>
<P>The first part of assembly language optimization, then, is self. An assembler is nothing more than a tool to let you design machine-language programs without having to think in hexadecimal codes. So assembly language programmers&#151;unlike all other programmers&#151;must take full responsibility for the quality of their code. Since assemblers provide little help at any level higher than the generation of machine language, the assembly programmer must be capable both of coding any programming construct directly and of controlling the PC at the lowest practical level&#151;the operating system, the BIOS, even the hardware where necessary. High-level languages handle most of this transparently to the programmer, but in assembly everything is fair&#151;and necessary&#151;game, which brings us to another aspect of assembly optimization: knowledge.
</P>
<H4 ALIGN="LEFT"><A NAME="Heading7"></A><FONT COLOR="#000077">Knowledge</FONT></H4>
<P>In the PC world, you can never have enough knowledge, and every item you add to your store will make your programs better. Thorough familiarity with both the operating system APIs and BIOS interfaces is important; since those interfaces are well-documented and reasonably straightforward, my advice is to get a good book or two and bring yourself up to speed. Similarly, familiarity with the PC hardware is required. While that topic covers a lot of ground&#151;display adapters, keyboards, serial ports, printer ports, timer and DMA channels, memory organization, and more&#151;most of the hardware is well-documented, and articles about programming major hardware components appear frequently in the literature, so this sort of knowledge can be acquired readily enough.
</P>
<P>The single most critical aspect of the hardware, and the one about which it is hardest to learn, is the CPU. The x86 family CPUs have a complex, irregular instruction set, and, unlike most processors, they are neither straightforward nor wellregarding true code performance. What&#146;s more, assembly is so difficult to learn that most articles and books that present assembly code settle for code that just works, rather than code that pushes the CPU to its limits. In fact, since most articles and books are written for inexperienced assembly programmers, there is very little information of any sort available about how to generate high-quality assembly code for the x86 family CPUs. As a result, knowledge about programming them effectively is by far the hardest knowledge to gather. A good portion of this book is devoted to seeking out such knowledge.</P>
<TABLE WIDTH="100%"><TD WIDTH="5%" VALIGN="TOP"><IMG SRC="images/02-02i.jpg"><TD WIDTH="95%"><SMALL><I>Be forewarned, though: No matter how much you learn about programming the PC in assembly, there&#146;s always more to discover.</I></SMALL>
</TABLE>
<P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="02-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="02-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

80
ch02/02-03.html Normal file
View file

@ -0,0 +1,80 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: A World Apart</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=02//-->
<!--PAGES=028-030//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="02-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch03/03-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H3><A NAME="Heading8"></A><FONT COLOR="#000077">The Flexible Mind</FONT></H3>
<P>Is the never-ending collection of information all there is to the assembly optimization, then? Hardly. Knowledge is simply a necessary base on which to build. Let&#146;s take a moment to examine the objectives of good assembly programming, and the remainder of the forces that act on assembly optimization will fall into place.
</P>
<P>Basically, there are only two possible objectives to high-performance assembly programming: Given the requirements of the application, keep to a minimum either the number of processor cycles the program takes to run, or the number of bytes in the program, or some combination of both. We&#146;ll look at ways to achieve both objectives, but we&#146;ll more often be concerned with saving cycles than saving bytes, for the PC generally offers relatively more memory than it does processing horsepower. In fact, we&#146;ll find that two-to-three times performance improvements <I>over already tight assembly code</I> are often possible if we&#146;re willing to spend additional bytes in order to save cycles. It&#146;s not always desirable to use such techniques to speed up code, due to the heavy memory requirements&#151;but it is almost always <I>possible</I>.</P>
<P>You will notice that my short list of objectives for high-performance assembly programming does not include traditional objectives such as easy maintenance and speed of development. Those are indeed important considerations&#151;to persons and companies that develop and distribute software. People who actually <I>buy</I> software, on the other hand, care only about how well that software performs, not how it was developed nor how it is maintained. These days, developers spend so much time focusing on such admittedly important issues as code maintainability and reusability, source code control, choice of development environment, and the like that they often forget rule #1: From the user&#146;s perspective, <I>performance is fundamental</I>.</P>
<TABLE WIDTH="100%"><TD WIDTH="5%" VALIGN="TOP"><IMG SRC="images/02-03i.jpg"><TD WIDTH="95%"><SMALL><I>Comment your code, design it carefully, and write non-time-critical portions in a high-level language, if you wish&#151;but when you write the portions that interact with the user and/or affect response time, performance must be your paramount objective, and assembly is the path to that goal</I>.</SMALL>
</TABLE>
<P>Knowledge of the sort described earlier is absolutely essential to fulfilling either of the objectives of assembly programming. What that knowledge doesn&#146;t do by itself is meet the need to write code that both performs to the requirements of the application at hand and also operates as efficiently as possible in the PC environment. Knowledge makes that possible, but your programming instincts make it happen. And it is that intuitive, on-the-fly integration of a program specification and a sea of facts about the PC that is the heart of the Zen-class assembly optimization.
</P>
<P>As with Zen of any sort, mastering that Zen of assembly language is more a matter of learning than of being taught. You will have to find your own path of learning, although I will start you on your way with this book. The subtle facts and examples I provide will help you gain the necessary experience, but you must continue the journey on your own. Each program you create will expand your programming horizons and increase the options available to you in meeting the next challenge. The ability of your mind to find surprising new and better ways to craft superior code from a concept&#151;the flexible mind, if you will&#151;is the linchpin of good assembler code, and you will develop this skill only by doing.</P>
<P>Never underestimate the importance of the flexible mind. Good assembly code is better than good compiled code. Many people would have you believe otherwise, but they&#146;re wrong. That doesn&#146;t mean that high-level languages are useless; far from it. High-level languages are the best choice for the majority of programmers, and for the bulk of the code of most applications. When the <I>best</I> code&#151;the fastest or smallest code possible&#151;is needed, though, assembly is the only way to go.</P>
<P>Simple logic dictates that no compiler can know as much about what a piece of code needs to do or adapt as well to those needs as the person who wrote the code. Given that superior information and adaptability, an assembly language programmer can generate better code than a compiler, all the more so given that compilers are constrained by the limitations of high-level languages and by the process of transformation from high-level to machine language. Consequently, carefully optimized assembly is not just the language of choice but the <I>only</I> choice for the 1percent to 10 percent of code&#151;usually consisting of small, well-defined subroutines&#151;that determines overall program performance, and it is the only choice for code that must be as compact as possible, as well. In the run-of-the-mill, non-time-critical portions of your programs, it makes no sense to waste time and effort on writing optimized assembly code&#151;concentrate your efforts on loops and the like instead; but in those areas where you need the finest code quality, accept no substitutes.</P>
<P>Note that I said that an assembly programmer <I>can</I> generate better code than a compiler, not <I>will</I> generate better code. While it is true that good assembly code is better than good compiled code, it is also true that bad assembly code is often much worse than bad compiled code; since the assembly programmer has so much control over the program, he or she has virtually unlimited opportunities to waste cycles and bytes. The sword cuts both ways, and good assembly code requires more, not less, forethought and planning than good code written in a high-level language.</P>
<P>The gist of all this is simply that good assembly programming is done in the context of a solid overall framework unique to each program, and the flexible mind is the key to creating that framework and holding it together.</P>
<H4 ALIGN="LEFT"><A NAME="Heading9"></A><FONT COLOR="#000077">Where to Begin?</FONT></H4>
<P>To summarize, the skill of assembly language optimization is a combination of knowledge, perspective, and a way of thought that makes possible the genesis of absolutely the fastest or the smallest code. With that in mind, what should the first step be? Development of the flexible mind is an obvious step. Still, the flexible mind is no better than the knowledge at its disposal. The first step in the journey toward mastering optimization at that exalted level, then, would seem to be learning how to learn.
</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="02-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch03/03-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

BIN
ch02/images/02-01.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
ch02/images/02-01i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch02/images/02-02.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
ch02/images/02-02i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch02/images/02-03i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

82
ch03/03-01.html Normal file
View file

@ -0,0 +1,82 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Assume Nothing</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=03//-->
<!--PAGES=031-035//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch02/02-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H2><A NAME="Heading1"></A><FONT COLOR="#000077">Chapter 3<BR>Assume Nothing
</FONT></H2>
<H3><A NAME="Heading2"></A><FONT COLOR="#000077">Understanding and Using the Zen Timer</FONT></H3>
<P>When you&#146;re pushing the envelope in writing optimized PC code, you&#146;re likely to become more than a little compulsive about finding approaches that let you wring more speed from your computer. In the process, you&#146;re bound to make mistakes, which is fine&#151;as long as you watch for those mistakes and <I>learn</I> from them.</P>
<P>A case in point: A few years back, I came across an article about 8088 assembly language called &#147;Optimizing for Speed.&#148; Now, &#147;optimize&#148; is not a word to be used lightly; <I>Webster&#146;s Ninth New Collegiate Dictionary</I> defines optimize as &#147;to make as perfect, effective, or functional as possible,&#148; which certainly leaves little room for error. The author had, however, chosen a small, well-defined 8088 assembly language routine to refine, consisting of about 30 instructions that did nothing more than expand 8 bits to 16 bits by duplicating each bit.</P>
<P>The author of &#147;Optimizing&#148; had clearly fine-tuned the code with care, examining alternative instruction sequences and adding up cycles until he arrived at an implementation he calculated to be nearly 50 percent faster than the original routine. In short, he had used all the information at his disposal to improve his code, and had, as a result, saved cycles by the bushel. There was, in fact, only one slight problem with the optimized version of the routine....</P>
<P>It ran slower than the original version!</P>
<H3><A NAME="Heading3"></A><FONT COLOR="#000077">The Costs of Ignorance</FONT></H3>
<P>As diligent as the author had been, he had nonetheless committed a cardinal sin of x86 assembly language programming: He had assumed that the information available to him was both correct and complete. While the execution times provided by Intel for its processors are indeed correct, they are incomplete; the other&#151;and often more important&#151;part of code performance is instruction <I>fetch</I> time, a topic to which I will return in later chapters.</P>
<P>Had the author taken the time to measure the true performance of his code, he wouldn&#146;t have put his reputation on the line with relatively low-performance code. What&#146;s more, had he actually measured the performance of his code and found it to be unexpectedly slow, curiosity might well have led him to experiment further and thereby add to his store of reliable information about the CPU.</P>
<TABLE WIDTH="100%"><TD WIDTH="5%" VALIGN="TOP"><IMG SRC="images/03-01i.jpg"><TD WIDTH="95%" VALIGN="TOP"><I><SMALL>There you have an important tenet of assembly language optimization: After crafting the best code possible, check it in action to see if it&#146;s really doing what you think it is. If it&#146;s not behaving as expected, that&#146;s all to the good, since solving mysteries is the path to knowledge. You&#146;ll learn more in this way, I assure you, than from any manual or book on assembly language.</I></SMALL>
</TABLE>
<P><I>Assume nothing</I>. I cannot emphasize this strongly enough&#151;when you care about performance, do your best to improve the code and then <I>measure</I> the improvement. If you don&#146;t measure performance, you&#146;re just guessing, and if you&#146;re guessing, you&#146;re not very likely to write top-notch code.</P>
<P>Ignorance about true performance can be costly. When I wrote video games for a living, I spent days at a time trying to wring more performance from my graphics drivers. I rewrote whole sections of code just to save a few cycles, juggled registers, and relied heavily on blurry-fast register-to-register shifts and adds. As I was writing my last game, I discovered that the program ran perceptibly faster if I used look-up tables instead of shifts and adds for my calculations. It <I>shouldn&#146;t</I> have run faster, according to my cycle counting, but it did. In truth, instruction fetching was rearing its head again, as it often does, and the fetching of the shifts and adds was taking as much as four times the nominal execution time of those instructions.</P>
<P>Ignorance can also be responsible for considerable wasted effort. I recall a debate in the letters column of one computer magazine about exactly how quickly text can be drawn on a Color/Graphics Adapter (CGA) screen without causing snow. The letter-writers counted every cycle in their timing loops, just as the author in the story that started this chapter had. Like that author, the letter-writers had failed to take the prefetch queue into account. In fact, they had neglected the effects of video wait states as well, so the code they discussed was actually <I>much</I> slower than their estimates. The proper test would, of course, have been to run the code to see if snow resulted, since the only true measure of code performance is observing it in action.</P>
<H3><A NAME="Heading4"></A><FONT COLOR="#000077">The Zen Timer</FONT></H3>
<P>Clearly, one key to mastering Zen-class optimization is a tool with which to measure code performance. The most accurate way to measure performance is with expensive hardware, but reasonable measurements at no cost can be made with the PC&#146;s 8253 timer chip, which counts at a rate of slightly over 1,000,000 times per second. The 8253 can be started at the beginning of a block of code of interest and stopped at the end of that code, with the resulting count indicating how long the code took to execute with an accuracy of about 1 microsecond. (A microsecond is one millionth of a second, and is abbreviated &#181;s). To be precise, the 8253 counts once every 838.1 nanoseconds. (A nanosecond is one billionth of a second, and is abbreviated ns.)
</P>
<P>Listing 3.1 shows 8253-based timer software, consisting of three subroutines: <B>ZTimerOn, ZTimerOff</B>, and <B>ZTimerReport</B>. For the remainder of this book, I&#146;ll refer to these routines collectively as the &#147;Zen timer.&#148; C-callable versions of the two precision Zen timers are presented in Chapter K on the companion CD-ROM.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch02/02-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

508
ch03/03-02.html Normal file
View file

@ -0,0 +1,508 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Assume Nothing</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=03//-->
<!--PAGES=035-042//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P><B>LISTING 3.1 PZTIMER.ASM</B></P>
<!-- CODE //-->
<PRE>
; The precision Zen timer (PZTIMER.ASM)
;
; Uses the 8253 timer to time the performance of code that takes
; less than about 54 milliseconds to execute, with a resolution
; of better than 10 microseconds.
;
; By Michael Abrash
;
; Externally callable routines:
;
; ZTimerOn: Starts the Zen timer, with interrupts disabled.
;
; ZTimerOff: Stops the Zen timer, saves the timer count,
; times the overhead code, and restores interrupts to the
; state they were in when ZTimerOn was called.
;
; ZTimerReport: Prints the net time that passed between starting
; and stopping the timer.
;
; Note: If longer than about 54 ms passes between ZTimerOn and
; ZTimerOff calls, the timer turns over and the count is
; inaccurate. When this happens, an error message is displayed
; instead of a count. The long-period Zen timer should be used
; in such cases.
;
; Note: Interrupts *MUST* be left off between calls to ZTimerOn
; and ZTimerOff for accurate timing and for detection of
; timer overflow.
;
; Note: These routines can introduce slight inaccuracies into the
; system clock count for each code section timed even if
; timer 0 doesn&#146;t overflow. If timer 0 does overflow, the
; system clock can become slow by virtually any amount of
; time, since the system clock can&#146;t advance while the
; precison timer is timing. Consequently, it&#146;s a good idea
; to reboot at the end of each timing session. (The
; battery-backed clock, if any, is not affected by the Zen
; timer.)
;
; All registers, and all flags except the interrupt flag, are
; preserved by all routines. Interrupts are enabled and then disabled
; by ZTimerOn, and are restored by ZTimerOff to the state they were
; in when ZTimerOn was called.
;
Code segment word public &#145;CODE&#146;
assumecs: Code, ds:nothing
public ZTimerOn, ZTimerOff, ZTimerReport
;
; Base address of the 8253 timer chip.
;
BASE_8253equ40h
;
; The address of the timer 0 count registers in the 8253.
;
TIMER_0_8253 equBASE_8253 &#43; 0
;
; The address of the mode register in the 8253.
;
MODE_8253 equBASE_8253 &#43; 3
;
; The address of Operation Command Word 3 in the 8259 Programmable
; Interrupt Controller (PIC) (write only, and writable only when
; bit 4 of the byte written to this address is 0 and bit 3 is 1).
;
OCW3 equ20h
;
; The address of the Interrupt Request register in the 8259 PIC
; (read only, and readable only when bit 1 of OCW3 = 1 and bit 0
; of OCW3 = 0).
;
IRR equ20h
;
; Macro to emulate a POPF instruction in order to fix the bug in some
; 80286 chips which allows interrupts to occur during a POPF even when
; interrupts remain disabled.
;
MPOPF macro
local p1, p2
jmp short p2
p1: iret ; jump to pushed address &amp pop flags
p2: push cs ; construct far return address to
call p1 ; the next instruction
endm
;
; Macro to delay briefly to ensure that enough time has elapsed
; between successive I/O accesses so that the device being accessed
; can respond to both accesses even on a very fast PC.
;
DELAY macro
jmp $&#43;2
jmp $&#43;2
jmp $&#43;2
endm
OriginalFlags db ? ; storage for upper byte of
; FLAGS register when
; ZTimerOn called
TimedCount dw ? ; timer 0 count when the timer
; is stopped
ReferenceCount dw ; number of counts required to
; execute timer overhead code
OverflowFlag db ? ; used to indicate whether the
; timer overflowed during the
; timing interval
;
; String printed to report results.
;
OutputStr label byte
db 0dh, 0ah, &#145;Timed count: &#145;, 5 dup (?)
ASCIICountEnd labelbyte
db &#145; microseconds&#146;, 0dh, 0ah
db &#145;$&#146;
;
; String printed to report timer overflow.
;
OverflowStr label byte
db 0dh, 0ah
db &#145;****************************************************&#146;
db 0dh, 0ah
db &#145;* The timer overflowed, so the interval timed was *&#146;
db 0dh, 0ah
db &#145;* too long for the precision timer to measure. *&#146;
db 0dh, 0ah
db &#145;* Please perform the timing test again with the *&#146;
db0dh, 0ah
db &#145;* long-period timer. *&#146;
db 0dh, 0ah
db &#145;****************************************************&#146;
db 0dh, 0ah
db &#145;$&#146;
; ********************************************************************
; * Routine called to start timing. *
; ********************************************************************
ZTimerOn proc near
;
; Save the context of the program being timed.
;
push ax
pushf
pop ax ; get flags so we can keep
; interrupts off when leaving
; this routine
mov cs:[OriginalFlags],ah ; remember the state of the
; Interrupt flag
and ah,0fdh ; set pushed interrupt flag
; to 0
push ax
;
; Turn on interrupts, so the timer interrupt can occur if it&#146;s
; pending.
;
sti
;
; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting. Also
; leaves the 8253 waiting for the initial timer 0 count to
; be loaded.
;
mov al,00110100b ;mode 2
out MODE_8253,al
;
; Set the timer count to 0, so we know we won&#146;t get another
; timer interrupt right away.
; Note: this introduces an inaccuracy of up to 54 ms in the system
; clock count each time it is executed.
;
DELAY
sub al,al
out TIMER_0_8253,al ;lsb
DELAY
out TIMER_0_8253,al ;msb
;
; Wait before clearing interrupts to allow the interrupt generated
; when switching from mode 3 to mode 2 to be recognized. The delay
; must be at least 210 ns long to allow time for that interrupt to
; occur. Here, 10 jumps are used for the delay to ensure that the
; delay time will be more than long enough even on a very fast PC.
;
rept 10
jmp $&#43;2
endm
;
; Disable interrupts to get an accurate count.
;
cli
;
; Set the timer count to 0 again to start the timing interval.
;
mov al,00110100b ; set up to load initial
out MODE_8253,al ; timer count
DELAY
sub al,al
out TIMER_0_8253,al ; load count lsb
DELAY
out TIMER_0_8253,al; load count msb
;
; Restore the context and return.
;
MPOPF ; keeps interrupts off
pop ax
ret
ZTimerOn endp
;********************************************************************
;* Routine called to stop timing and get count. *
;********************************************************************
ZTimerOff proc near
;
; Save the context of the program being timed.
;
push ax
push cx
pushf
;
; Latch the count.
;
mov al,00000000b ; latch timer 0
out MODE_8253,al
;
; See if the timer has overflowed by checking the 8259 for a pending
; timer interrupt.
;
mov al,00001010b ; OCW3, set up to read
out OCW3,al; Int errupt Request register
DELAY
ina l,IRR; read Interrupt Request
; register
and al,1 ; set AL to 1 if IRQ0 (the
; timer interrupt) is pending
mov cs:[OverflowFlag],al; store the timer overflow
; status
;
; Allow interrupts to happen again.
;
sti
;
; Read out the count we latched earlier.
;
in al,TIMER_0_8253 ; least significant byte
DELAY
mov ah,al
in al,TIMER_0_8253 ; most significant byte
xchg ah,al
neg ax ; convert from countdown
; remaining to elapsed
; count
mov cs:[TimedCount],ax
; Time a zero-length code fragment, to get a reference for how
; much overhead this routine has. Time it 16 times and average it,
; for accuracy, rounding the result.
;
mov cs:[ReferenceCount],0
mov cx,16
cli ; interrupts off to allow a
; precise reference count
RefLoop:
call ReferenceZTimerOn
call ReferenceZTimerOff
loop RefLoop
sti
add cs:[ReferenceCount],8; total &#43; (0.5 * 16)
mov cl,4
shr cs:[ReferenceCount],cl; (total) / 16 &#43; 0.5
;
; Restore original interrupt state.
;
pop ax ; retrieve flags when called
mov ch,cs:[OriginalFlags] ; get back the original upper
; byte of the FLAGS register
and ch,not 0fdh ; only care about original
; interrupt flag...
and ah,0fdh ; ...keep all other flags in
; their current condition
or ah,ch ; make flags word with original
; interrupt flag
push ax ; prepare flags to be popped
;
; Restore the context of the program being timed and return to it.
;
MPOPF ; restore the flags with the
; original interrupt state
pop cx
pop ax
ret
ZTimerOff endp
;
; Called by ZTimerOff to start timer for overhead measurements.
;
ReferenceZTimerOnproc near
;
; Save the context of the program being timed.
;
push ax
pushf ; interrupts are already off
;
; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting.
;
mov al,00110100b ; set up to load
out MODE_8253,al ; initial timer count
DELAY
;
; Set the timer count to 0.
;
sub al,al
out TIMER_0_8253,al; load count lsb
DELAY
out TIMER_0_8253,al; load count msb
;
; Restore the context of the program being timed and return to it.
;
MPOPF
pop ax
ret
ReferenceZTimerOnendp
;
; Called by ZTimerOff to stop timer and add result to ReferenceCount
; for overhead measurements.
;
ReferenceZTimerOff proc near
;
; Save the context of the program being timed.
;
push ax
push cx
pushf
;
; Latch the count and read it.
;
mov al,00000000b ; latch timer 0
out MODE_8253,al
DELAY
in al,TIMER_0_8253 ; lsb
DELAY
mov ah,al
in al,TIMER_0_8253 ; msb
xchg ah,al
neg ax ; convert from countdown
; remaining to amount
; counted down
add cs:[ReferenceCount],ax
;
; Restore the context of the program being timed and return to it.
;
MPOPF
pop cx
pop ax
ret
ReferenceZTimerOff endp
; ********************************************************************
; * Routine called to report timing results. *
; ********************************************************************
ZTimerReport procnear
pushf
push ax
push bx
push cx
push dx
push si
push ds
;
push cs ; DOS functions require that DS point
pop ds ; to text to be displayed on the screen
assume ds :Code
;
; Check for timer 0 overflow.
;
cmp [OverflowFlag],0
jz PrintGoodCount
mov dx,offset OverflowStr
mov ah,9
int 21h
jmp short EndZTimerReport
;
; Convert net count to decimal ASCII in microseconds.
;
PrintGoodCount:
mov ax,[TimedCount]
sub ax,[ReferenceCount]
mov si,offset ASCIICountEnd - 1
;
; Convert count to microseconds by multiplying by .8381.
;
mov dx, 8381
mul dx
mov bx, 10000
div bx ;* .8381 = * 8381 / 10000
;
; Convert time in microseconds to 5 decimal ASCII digits.
;
mov bx, 10
mov cx, 5
CTSLoop:
sub dx, dx
div bx
add dl,&#146;0&#146;
mov [si],dl
dec si
loop CTSLoop
;
; Print the results.
;
mov ah, 9
mov dx, offset OutputStr
int 21h
;
EndZTimerReport:
pop ds
pop si
pop dx
pop cx
pop bx
pop ax
MPOPF
ret
ZTimerReport endp
Code ends
end
</PRE>
<!-- END CODE //-->
<P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

85
ch03/03-03.html Normal file
View file

@ -0,0 +1,85 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Assume Nothing</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=03//-->
<!--PAGES=042-045//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-04.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H4 ALIGN="LEFT"><A NAME="Heading5"></A><FONT COLOR="#000077">The Zen Timer Is a Means, Not an End</FONT></H4>
<P>We&#146;re going to spend the rest of this chapter seeing what the Zen timer can do, examining how it works, and learning how to use it. I&#146;ll be using the Zen timer again and again over the course of this book, so it&#146;s essential that you learn what the Zen timer can do and how to use it. On the other hand, it is by no means essential that you understand exactly how the Zen timer works. (Interesting, yes; essential, no.)
</P>
<P>In other words, the Zen timer isn&#146;t really part of the knowledge we seek; rather, it&#146;s one tool with which we&#146;ll acquire that knowledge. Consequently, you shouldn&#146;t worry if you don&#146;t fully grasp the inner workings of the Zen timer. Instead, focus on learning how to <I>use</I> it, and you&#146;ll be on the right road.</P>
<H4 ALIGN="LEFT"><A NAME="Heading6"></A><FONT COLOR="#000077">Starting the Zen Timer</FONT></H4>
<P><B>ZTimerOn</B> is called at the start of a segment of code to be timed. <B>ZTimerOn</B> saves the context of the calling code, disables interrupts, sets timer 0 of the 8253 to mode 2 (divide-by-N mode), sets the initial timer count to 0, restores the context of the calling code, and returns. (I&#146;d like to note that while Intel&#146;s documentation for the 8253 seems to indicate that a timer won&#146;t reset to 0 until it finishes counting down, in actual practice, timers seem to reset to 0 as soon as they&#146;re loaded.)</P>
<P>Two aspects of <B>ZTimerOn</B> are worth discussing further. One point of interest is that <B>ZTimerOn</B> disables interrupts. (<B>ZTimerOff</B> later restores interrupts to the state they were in when <B>ZTimerOn</B> was called.) Were interrupts not disabled by <B>ZTimerOn</B>, keyboard, mouse, timer, and other interrupts could occur during the timing interval, and the time required to service those interrupts would incorrectly and erratically appear to be part of the execution time of the code being measured. As a result, code timed with the Zen timer should not expect any hardware interrupts to occur during the interval between any call to <B>ZTimerOn</B> and the corresponding call to <B>ZTimerOff</B>, and should not enable interrupts during that time.</P>
<H3><A NAME="Heading7"></A><FONT COLOR="#000077">Time and the PC</FONT></H3>
<P>A second interesting point about <B>ZTimerOn</B> is that it may introduce some small inaccuracy into the system clock time whenever it is called. To understand why this is so, we need to examine the way in which both the 8253 and the PC&#146;s system clock (which keeps the current time) work.</P>
<P>The 8253 actually contains three timers, as shown in Figure 3.1. All three timers are driven by the system board&#146;s 14.31818 MHz crystal, divided by 12 to yield a 1.19318 MHz clock to the timers, so the timers count once every 838.1 ns. Each of the three timers counts down in a programmable way, generating a signal on its output pin when it counts down to 0. Each timer is capable of being halted at any time via a 0 level on its gate input; when a timer&#146;s gate input is 1, that timer counts constantly. All in all, the 8253&#146;s timers are inherently very flexible timing devices; unfortunately, much of that flexibility depends on how the timers are connected to external circuitry, and in the PC the timers are connected with specific purposes in mind.</P>
<P>Timer 2 drives the speaker, although it can be used for other timing purposes when the speaker is not in use. As shown in Figure 3.1, timer 2 is the only timer with a programmable gate input in the PC; that is, timer 2 is the only timer that can be started and stopped under program control in the manner specified by Intel. On the other hand, the <I>output</I> of timer 2 is connected to nothing other than the speaker. In particular, timer 2 cannot generate an interrupt to get the 8088&#146;s attention.</P>
<P>Timer 1 is dedicated to providing dynamic RAM refresh, and should not be tampered with lest system crashes result.</P>
<P><A NAME="Fig1"><!-- </A><A HREF="javascript:displayWindow('images/03-01.jpg',410,243 )"> --><IMG SRC="images/03-01.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/03-01.jpg',410,243)"> --><FONT COLOR="#000077"><B>Figure 3.1</B></FONT></A>&nbsp;&nbsp;<I>The configuration of the 8253 timer chip in the PC.</I>
</P>
<P>Finally, timer 0 is used to drive the system clock. As programmed by the BIOS at power-up, every 65,536 (64K) counts, or 54.925 milliseconds, timer 0 generates a rising edge on its output line. (A millisecond is one-thousandth of a second, and is abbreviated ms.) This line is connected to the hardware interrupt 0 (IRQ0) line on the system board, so every 54.925 ms, timer 0 causes hardware interrupt 0 to occur.
</P>
<P>The interrupt vector for IRQ0 is set by the BIOS at power-up time to point to a BIOS routine, <B>TIMER_INT,</B> that maintains a time-of-day count. <B>TIMER_INT</B> keeps a 16-bit count of IRQ0 interrupts in the BIOS data area at address 0000:046C (all addresses in this book are given in segment:offset hexadecimal pairs); this count turns over once an hour (less a few microseconds), and when it does, <B>TIMER_INT</B> updates a 16-bit hour count at address 0000:046E in the BIOS data area. This count is the basis for the current time and date that DOS supports via functions 2AH (2A hexadecimal) through 2DH and by way of the DATE and TIME commands.</P>
<P>Each timer channel of the 8253 can operate in any of six modes. Timer 0 normally operates in mode 3: <I>square wave mode</I>. In square wave mode, the initial count is counted down two at a time; when the count reaches zero, the output state is changed. The initial count is again counted down two at a time, and the output state is toggled back when the count reaches zero. The result is a square wave that changes state more slowly than the input clock by a factor of the initial count. In its normal mode of operation, timer 0 generates an output pulse that is low for about 27.5 ms and high for about 27.5 ms; this pulse is sent to the 8259 interrupt controller, and its rising edge generates a timer interrupt once every 54.925 ms.</P>
<P>Square wave mode is not very useful for precision timing because it counts down by two twice per timer interrupt, thereby rendering exact timings impossible. Fortunately, the 8253 offers another timer mode, mode 2 (divide-by-N mode), which is both a good substitute for square wave mode and a perfect mode for precision timing.</P>
<P>Divide-by-N mode counts down by one from the initial count. When the count reaches zero, the timer turns over and starts counting down again without stopping, and a pulse is generated for a single clock period. While the pulse is not held for nearly as long as in square wave mode, it doesn&#146;t matter, since the 8259 interrupt controller is configured in the PC to be edgeand hence cares only about the existence of a pulse from timer 0, not the duration of the pulse. As a result, timer 0 continues to generate timer interrupts in divide-by-N mode, and the system clock continues to maintain good time.</P>
<P>Why not use timer 2 instead of timer 0 for precision timing? After all, timer 2 has a programmable gate input and isn&#146;t used for anything but sound generation. The problem with timer 2 is that its output can&#146;t generate an interrupt; in fact, timer 2 can&#146;t do anything but drive the speaker. We need the interrupt generated by the output of timer 0 to tell us when the count has overflowed, and we will see shortly that the timer interrupt also makes it possible to time much longer periods than the Zen timer shown in Listing 3.1 supports.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-04.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

84
ch03/03-04.html Normal file
View file

@ -0,0 +1,84 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Assume Nothing</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=03//-->
<!--PAGES=045-048//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-05.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>In fact, the Zen timer shown in Listing 3.1 can only time intervals of up to about 54 ms in length, since that is the period of time that can be measured by timer 0 before its count turns over and repeats. fifty-four ms may not seem like a very long time, but even a CPU as slow as the 8088 can perform more than 1,000 divides in 54 ms, and division is the single instruction that the 8088 performs most slowly. If a measured period turns out to be longer than 54 ms (that is, if timer 0 has counted down and turned over), the Zen timer will display a message to that effect. A long-period Zen timer for use in such cases will be presented later in this chapter.
</P>
<P>The Zen timer determines whether timer 0 has turned over by checking to see whether an IRQ0 interrupt is pending. (Remember, interrupts are off while the Zen timer runs, so the timer interrupt cannot be recognized until the Zen timer stops and enables interrupts.) If an IRQ0 interrupt is pending, then timer 0 has turned over and generated a timer interrupt. Recall that <B>ZTimerOn</B> initially sets timer 0 to 0, in order to allow for the longest possible period&#151;about 54 ms&#151;before timer 0 reaches 0 and generates the timer interrupt.</P>
<P>Now we&#146;re ready to look at the ways in which the Zen timer can introduce inaccuracy into the system clock. Since timer 0 is initially set to 0 by the Zen timer, and since the system clock ticks only when timer 0 counts off 54.925 ms and reaches 0 again, an average inaccuracy of one-half of 54.925 ms, or about 27.5 ms, is incurred each time the Zen timer is started. In addition, a timer interrupt is generated when timer 0 is switched from mode 3 to mode 2, advancing the system clock by up to 54.925 ms, although this only happens the first time the Zen timer is run after a warm or cold boot. Finally, up to 54.925 ms can again be lost when <B>ZTimerOff</B> is called, since that routine again sets the timer count to zero. Net result: The system clock will run up to 110 ms (about a ninth of a second) slow each time the Zen timer is used.</P>
<P>Potentially far greater inaccuracy can be incurred by timing code that takes longer than about 110 ms to execute. Recall that all interrupts, including the timer interrupt, are disabled while timing code with the Zen timer. The 8259 interrupt controller is capable of remembering at most one pending timer interrupt, so all timer interrupts after the first one during any given Zen timing interval are ignored. Consequently, if a timing interval exceeds 54.9 ms, the system clock effectively stops 54.9 ms after the timing interval starts and doesn&#146;t restart until the timing interval ends, losing time all the while.</P>
<P>The effects on the system time of the Zen timer aren&#146;t a matter for great concern, as they are temporary, lasting only until the next warm or cold boot. System that have batteryclocks, (AT-style machines; that is, virtually all machines in common use) automatically reset the correct time whenever the computer is booted, and systems without battery-clocks prompt for the correct date and time when booted. Also,repeated use of the Zen timer usually makes the system clock slow by at most a total of a few seconds, unless code that takes much longer than 54 ms to run is timed (in which case the Zen timer will notify you that the code is too long to time).</P>
<P>Nonetheless, it&#146;s a good idea to reboot your computer at the end of each session with the Zen timer in order to make sure that the system clock is correct.</P>
<H3><A NAME="Heading8"></A><FONT COLOR="#000077">Stopping the Zen Timer</FONT></H3>
<P>At some point after <B>ZTimerOn</B> is called, <B>ZTimerOff</B> must always be called to mark the end of the timing interval. <B>ZTimerOff</B> saves the context of the calling program, latches and reads the timer 0 count, converts that count from the countdown value that the timer maintains to the number of counts elapsed since <B>ZTimerOn</B> was called, and stores the result. Immediately after latching the timer 0 count&#151;and before enabling interrupts&#151;<B>ZTimerOff</B> checks the 8259 interrupt controller to see if there is a pending timer interrupt, setting a flag to mark that the timer overflowed if there is indeed a pending timer interrupt.</P>
<P>After that, <B>ZTimerOff</B> executes just the overhead code of <B>ZTimerOn</B> and <B>ZTimerOff</B> 16 times, and averages and saves the results in order to determine how many of the counts in the timing result just obtained were incurred by the overhead of the Zen timer rather than by the code being timed.</P>
<P>Finally, <B>ZTimerOff</B> restores the context of the calling program, including the state of the interrupt flag that was in effect when <B>ZTimerOn</B> was called to start timing, and returns.</P>
<P>One interesting aspect of <B>ZTimerOff</B> is the manner in which timer 0 is stopped in order to read the timer count. We don&#146;t actually have to stop timer 0 to read the count; the 8253 provides a special latched read feature for the specific purpose of reading the count while a time is running. (That&#146;s a good thing, too; we&#146;ve no documented way to stop timer 0 if we wanted to, since its gate input isn&#146;t connected. Later in this chapter, though, we&#146;ll see that timer 0 can be stopped after all.) We simply tell the 8253 to latch the current count, and the 8253 does so without breaking stride.</P>
<H3><A NAME="Heading9"></A><FONT COLOR="#000077">Reporting Timing Results</FONT></H3>
<P><B>ZTimerReport</B> may be called to display timing results at any time after both <B>ZTimerOn</B> and <B>ZTimerOff</B> have been called. <B>ZTimerReport</B> first checks to see whether the timer overflowed (counted down to 0 and turned over) before <B>ZTimerOff</B> was called; if overflow did occur, <B>ZTimerOff</B> prints a message to that effect and returns. Otherwise, <B>ZTimerReport</B> subtracts the reference count (representing the overhead of the Zen timer) from the count measured between the calls to <B>ZTimerOn</B> and <B>ZTimerOff</B>, converts the result from timer counts to microseconds, and prints the resulting time in microseconds to the standard output.</P>
<P>Note that <B>ZTimerReport</B> need not be called immediately after <B>ZTimerOff</B>. In fact, after a given call to <B>ZTimerOff, ZTimerReport</B> can be called at any time right up until the next call to <B>ZTimerOn</B>.</P>
<P>You may want to use the Zen timer to measure several portions of a program while it executes normally, in which case it may not be desirable to have the text printed by <B>ZTimerReport</B> interfere with the program&#146;s normal display. There are many ways to deal with this. One approach is removal of the invocations of the DOS print string function (INT 21H with AH equal to 9) from <B>ZTimerReport</B>, instead running the program under a debugger that supports screen flipping (such as Turbo Debugger or CodeView), placing a breakpoint at the start of <B>ZTimerReport</B>, and directly observing the count in microseconds as <B>ZTimerReport</B> calculates it.</P>
<P>A second approach is modification of <B>ZTimerReport</B> to place the result at some safe location in memory, such as an unused portion of the BIOS data area.</P>
<P>A third approach is alteration of <B>ZTimerReport</B> to print the result over a serial port to a terminal or to another PC acting as a terminal. Similarly, many debuggers can be run from a remote terminal via a serial link.</P>
<P>Yet another approach is modification of <B>ZTimerReport</B> to send the result to the printer via either DOS function 5 or BIOS interrupt 17H.</P>
<P>A final approach is to modify <B>ZTimerReport</B> to print the result to the auxiliary output via DOS function 4, and to then write and load a special device driver named <B>AUX</B>, to which DOS function 4 output would automatically be directed. This device driver could send the result anywhere you might desire. The result might go to the secondary display adapter, over a serial port, or to the printer, or could simply be stored in a buffer within the driver, to be dumped at a later time. (Credit for this final approach goes to Michael Geary, and thanks go to David Miller for passing the idea on to me.)</P>
<P>You may well want to devise still other approaches better suited to your needs than those I&#146;ve presented. Go to it! I&#146;ve just thrown out a few possibilities to get you started.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-05.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

145
ch03/03-05.html Normal file
View file

@ -0,0 +1,145 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Assume Nothing</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=03//-->
<!--PAGES=048-050//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-04.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-06.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H3><A NAME="Heading10"></A><FONT COLOR="#000077">Notes on the Zen Timer</FONT></H3>
<P>The Zen timer subroutines are designed to be near-called from assembly language code running in the public segment <B>Code</B>. The Zen timer subroutines can, however, be called from any assembly or high-level language code that generates OBJ files that are compatible with the Microsoft linker, simply by modifying the segment that the timer code runs in to match the segment used by the code being timed, or by changing the Zen timer routines to far procedures and making far calls to the Zen timer code from the code being timed, as discussed at the end of this chapter. All three subroutines preserve all registers and all flags except the interrupt flag, so calls to these routines are transparent to the calling code.</P>
<P>If you do change the Zen timer routines to far procedures in order to call them from code running in another segment, be sure to make <I>all</I> the Zen timer routines far, including <B>ReferenceZTimerOn</B> and <B>ReferenceZTimerOff</B>. (You&#146;ll have to put <B>FAR PTR</B> overrides on the calls from <B>ZTimerOff</B> to the latter two routines if you do make them far.) If the reference routines aren&#146;t the same type&#151;near or far&#151;as the other routines, they won&#146;t reflect the true overhead incurred by starting and stopping the Zen timer.</P>
<P>Please be aware that the inaccuracy that the Zen timer can introduce into the system clock time does not affect the accuracy of the performance measurements reported by the Zen timer itself. The 8253 counts once every 838 ns, giving us a count resolution of about 1&#181;s, although factors such as the prefetch queue (as discussed below), dynamic RAM refresh, and internal timing variations in the 8253 make it perhaps more accurate to describe the Zen timer as measuring code performance with an accuracy of better than 10&#181;s. In fact, the Zen timer is actually most accurate in assessing code performance when timing intervals longer than about 100 &#181;s. At any rate, we&#146;re most interested in using the Zen timer to assess the relative performance of various code sequences&#151;that is, using it to compare and tweak code&#151;and the timer is more than accurate enough for that purpose.</P>
<P>The Zen timer works on all PC-compatible computers I&#146;ve tested it on, including XTs, ATs, PS/2 computers, and 386, 486, and Pentium-based machines. Of course, I haven&#146;t been able to test it on <I>all</I> PC-compatibles, but I don&#146;t expect any problems; computers on which the Zen timer doesn&#146;t run can&#146;t truly be called &#147;PC-compatible.&#148;</P>
<P>On the other hand, there is certainly no guarantee that code performance as measured by the Zen timer will be the same on compatible computers as on genuine IBM machines, or that either absolute or relative code performance will be similar even on different IBM models; in fact, quite the opposite is true. For example, every PS/2 computer, even the relatively slow Model 30, executes code much faster than does a PC or XT. As another example, I set out to do the timings for my earlier book <I>Zen of Assembly Language</I> on an XTcomputer, only to find that the computer wasn&#146;t quite IBM-compatible regarding code performance. The differences were minor, mind you, but my experience illustrates the risk of assuming that a specific make of computer will perform in a certain way without actually checking.</P>
<P>Not that this variation between models makes the Zen timer one whit less useful&#151;quite the contrary. The Zen timer is an excellent tool for evaluating code performance over the entire spectrum of PC-compatible computers.</P>
<H3><A NAME="Heading11"></A><FONT COLOR="#000077">A Sample Use of the Zen Timer</FONT></H3>
<P>Listing 3.2 shows a test-bed program for measuring code performance with the Zen timer. This program sets DS equal to CS (for reasons we&#146;ll discuss shortly), includes the code to be measured from the file TESTCODE, and calls <B>ZTimerReport</B> to display the timing results. Consequently, the code being measured should be in the file TESTCODE, and should contain calls to <B>ZTimerOn</B> and <B>ZTimerOff</B> .</P>
<P><B>LISTING 3.2 PZTEST.ASM</B></P>
<!-- CODE //-->
<PRE>
; Program to measure performance of code that takes less than
; 54 ms to execute. (PZTEST.ASM)
;
; Link with PZTIMER.ASM (Listing 3.1). PZTEST.BAT (Listing 3.4)
; can be used to assemble and link both files. Code to be
; measured must be in the file TESTCODE; Listing 3.3 shows
; a sample TESTCODE file.
;
; By Michael Abrash
;
mystack segment para stack &#145;STACK&#146;
db 512 dup(?)
mystack ends
;
Code segment para public &#145;CODE&#146;
assume cs:Code, ds:Code
extrnZTimerOn:near, ZTimerOff:near, ZTimerReport:near
Start proc near
push cs
pop ds ; set DS to point to the code segment,
; so data as well as code can easily
; be included in TESTCODE
;
include TESTCODE ;code to be measured, including
; calls to ZTimerOn and ZTimerOff
;
; Display the results.
;
call ZTimerReport
;
; Terminate the program.
;
mov ah,4ch
int 21h
Start endp
Code ends
end Start
</PRE>
<!-- END CODE //-->
<P>Listing 3.3 shows some sample code to be timed. This listing measures the time required to execute 1,000 loads of AL from the memory variable <B>MemVar</B> . Note that Listing 3.3 calls <B>ZTimerOn</B> to start timing, performs 1,000 <B>MOV</B> instructions in a row, and calls <B>ZTimerOff</B> to end timing. When Listing 3.2 is named TESTCODE and included by Listing 3.3, Listing 3.2 calls <B>ZTimerReport</B> to display the execution time after the code in Listing 3.3 has been run.</P>
<P><B>LISTING 3.3 LST3-3.ASM</B></P>
<!-- CODE //-->
<PRE>
; Test file;
; Measures the performance of 1,000 loads of AL from
; memory. (Use by renaming to TESTCODE, which is
; included by PZTEST.ASM (Listing 3.2). PZTIME.BAT
; (Listing 3.4) does this, along with all assembly
; and linking.)
;
jmp Skip ;jump around defined data
;
MemVar db ?
;
Skip:
;
; Start timing.
;
call ZTimerOn
;
rept 1000
mov al,[MemVar]
endm
;
; Stop timing.
;
call ZTimerOff
</PRE>
<!-- END CODE //-->
<P>It&#146;s worth noting that Listing 3.3 begins by jumping around the memory variable <B>MemVar</B>. This approach lets us avoid reproducing Listing 3.2 in its entirety for each code fragment we want to measure; by defining any needed data right in the code segment and jumping around that data, each listing becomes self-contained and can be plugged directly into Listing 3.2 as TESTCODE. Listing 3.2 sets DS equal to CS before doing anything else precisely so that data can be embedded in code fragments being timed. Note that only after the initial jump is performed in Listing 3.3 is the Zen timer started, since we don&#146;t want to include the execution time of start-up code in the timing interval. That&#146;s why the calls to <B>ZTimerOn</B> and <B>ZTimerOff</B> are in TESTCODE, not in PZTEST.ASM; this way, we have full control over which portion of TESTCODE is timed, and we can keep set-up code and the like out of the timing interval.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-04.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-06.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

154
ch03/03-06.html Normal file
View file

@ -0,0 +1,154 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Assume Nothing</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=03//-->
<!--PAGES=051-053//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-05.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-07.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>Listing 3.3 is used by naming it TESTCODE, assembling both Listing 3.2 (which includes TESTCODE) and Listing 3.1 with TASM or MASM, and linking the two resulting OBJ files together by way of the Borland orMicrosoft linker. Listing 3.4 shows a batch file, PZTIME.BAT, which does all that; when run, this batch file generates and runs the executable file PZTEST.EXE. PZTIME.BAT (Listing 3.4) assumes that the file PZTIMER.ASM contains Listing 3.1, and the file PZTEST.ASM contains Listing 3.2. The command-line parameter to PZTIME.BAT is the name of the file to be copied to TESTCODE and included into PZTEST.ASM. (Note that Turbo Assembler can be substituted for MASM by replacing &#147;masm&#148; with &#147;tasm&#148; and &#147;link&#148; with &#147;tlink&#148; in Listing 3.4. The same is true of Listing 3.7.)
</P>
<P><B>LISTING 3.4 PZTIME.BAT</B></P>
<!-- CODE //-->
<PRE>
echo off
rem
rem *** Listing 3.4 ***
rem
rem ***************************************************************
rem * Batch file PZTIME.BAT, which builds and runs the precision *
rem * Zen timer program PZTEST.EXE to time the code named as the *
rem * command-line parameter. Listing 3.1 must be named *
rem * PZTIMER.ASM, and Listing 3.2 must be named PZTEST.ASM. To *
rem * time the code in LST3-3, you&#146;d type the DOS command: *
rem * *
rem * pztime lst3-3 *
rem * *
rem * Note that MASM and LINK must be in the current directory or *
rem * on the current path in order for this batch file to work. *
rem * *
rem * This batch file can be speeded up by assembling PZTIMER.ASM *
rem * once, then removing the lines: *
rem * *
rem * masm pztimer; *
rem * if errorlevel 1 goto errorend *
rem * *
rem * from this file. *
rem * *
rem * By Michael Abrash *
rem ***************************************************************
rem
rem Make sure a file to test was specified.
rem
if not x%1==x goto ckexist
echo ***************************************************************
echo * Please specify a file to test. *
echo ***************************************************************
goto end
rem
rem Make sure the file exists.
rem
:ckexist
if exist %1 goto docopy
echo ***************************************************************
echo * The specified file, &#147;%1,&#148; doesn&#146;t exist. *
echo ***************************************************************
goto end
rem
rem copy the file to measure to TESTCODE.
rem
:docopy
copy %1 testcode
masm pztest;
if errorlevel 1 goto errorend
masm pztimer;
if errorlevel 1 goto errorend
link pztest&#43;pztimer;
if errorlevel 1 goto errorend
pztest
goto end
:errorend
echo ***************************************************************
echo * An error occurred while building the precision Zen timer. *
echo ***************************************************************
:end
</PRE>
<!-- END CODE //-->
<P>Assuming that Listing 3.3 is named LST3-3.ASM and Listing 3.4 is named PZTIME.BAT, the code in Listing 3.3 would be timed with the command:
</P>
<!-- CODE SNIP //-->
<PRE>
pztime LST3-3.ASM
</PRE>
<!-- END CODE SNIP //-->
<P>which performs all assembly and linking, and reports the execution time of the code in Listing 3.3.
</P>
<P>When the above command is executed on an original 4.77 MHz IBM PC, the time reported by the Zen timer is 3619 &#181;s, or about 3.62 &#181;s per load of AL from memory. (While the exact number is 3.619 &#181;s per load of AL, I&#146;m going to round off that last digit from now on. No matter how many repetitions of a given instruction are timed, there&#146;s just too much noise in the timing process&#151;between dynamic RAM refresh, the prefetch queue, and the internal state of the processor at the start of timing&#151;for that last digit to have any significance.) Given the test PC&#146;s 4.77 MHz clock, this works out to about 17 cycles per <B>MOV</B>, which is actually a good bit longer than Intel&#146;s specified 10-cycle execution time for this instruction. (See the MASM or TASM documentation, or Intel&#146;s processor reference manuals, for official execution times.) Fear not, the Zen timer is right&#151;<B>MOV AL,[MEMVAR]</B> really does take 17 cycles as used in Listing 3.3. Exactly why that is so is just what this book is all about.</P>
<P>In order to perform any of the timing tests in this book, enter Listing 3.1 and name it PZTIMER.ASM, enter Listing 3.2 and name it PZTEST.ASM, and enter Listing 3.4 and name it PZTIME.BAT. Then simply enter the listing you wish to run into the file <I>filename</I> and enter the command:</P>
<!-- CODE SNIP //-->
<PRE>
pztime &ltfilename&gt
</PRE>
<!-- END CODE SNIP //-->
<P>In fact, that&#146;s exactly how I timed each of the listings in this book. Code fragments you write yourself can be timed in just the same way. If you wish to time code directly in place in your programs, rather than in the test-bed program of Listing 3.2, simply insert calls to <B>ZTimerOn, ZTimerOff</B>, and <B>ZTimerReport</B> in the appropriate places and link PZTIMER to your program.</P>
<H3><A NAME="Heading12"></A><FONT COLOR="#000077">The Long-Period Zen Timer</FONT></H3>
<P>With a few exceptions, the Zen timer presented above will serve us well for the remainder of this book since we&#146;ll be focusing on relatively short code sequences that generally take much less than 54 ms to execute. Occasionally, however, we will need to time longer intervals. What&#146;s more, it is very likely that you will want to time code sequences longer than 54 ms at some point in your programming career. Accordingly, I&#146;ve also developed a Zen timer for periods longer than 54 ms. The long-period Zen timer (so named by contrast with the precision Zen timer just presented) shown in Listing 3.5 can measure periods up to one hour in length.
</P>
<P>The key difference between the long-period Zen timer and the precision Zen timer is that the long-period timer leaves interrupts enabled during the timing period. As a result, timer interrupts are recognized by the PC, allowing the BIOS to maintain an accurate system clock time over the timing period. Theoretically, this enables measurement of arbitrarily long periods. Practically speaking, however, there is no need for a timer that can measure more than a few minutes, since the DOS time of day and date functions (or, indeed, the DATE and TIME commands in a batch file) serve perfectly well for longer intervals. Since very long timing intervals aren&#146;t needed, the long-period Zen timer uses a simplified means of calculating elapsed time that is limited to measuring intervals of an hour or less. If a period longer than an hour is timed, the long-period Zen timer prints a message to the effect that it is unable to time an interval of that length.</P>
<P>For implementation reasons, the long-period Zen timer is also incapable of timing code that starts before midnight and ends after midnight; if that eventuality occurs, the long-period Zen timer reports that it was unable to time the code because midnight was crossed. If this happens to you, just time the code again, secure in the knowledge that at least you won&#146;t run into the problem again for 23-odd hours.</P>
<P>You should not use the long-period Zen timer to time code that requires interrupts to be disabled for more than 54 ms at a stretch during the timing interval, since when interrupts are disabled the long-period Zen timer is subject to the same 54 ms maximum measurement time as the precision Zen timer.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-05.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-07.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

713
ch03/03-07.html Normal file
View file

@ -0,0 +1,713 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Assume Nothing</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=03//-->
<!--PAGES=053-065//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-06.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-08.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>While permitting the timer interrupt to occur allows long intervals to be timed, that same interrupt makes the long-period Zen timer less accurate than the precision Zen timer, since the time the BIOS spends handling timer interrupts during the timing interval is included in the time measured by the long-period timer. Likewise, any other interrupts that occur during the timing interval, most notably keyboard and mouse interrupts, will increase the measured time.
</P>
<P>The long-period Zen timer has some of the same effects on the system time as does the precision Zen timer, so it&#146;s a good idea to reboot the system after a session with the long-period Zen timer. The long-period Zen timer does not, however, have the same potential for introducing major inaccuracy into the system clock time during a single timing run since it leaves interrupts enabled and therefore allows the system clock to update normally.</P>
<H4 ALIGN="LEFT"><A NAME="Heading13"></A><FONT COLOR="#000077">Stopping the Clock</FONT></H4>
<P>There&#146;s a potential problem with the long-period Zen timer. The problem is this: In order to measure times longer than 54 ms, we must maintain not one but two timing components, the timer 0 count and the BIOS time-of-day count. The time-of-day count measures the passage of 54.9 ms intervals, while the timer 0 count measures time within those 54.9 ms intervals. We need to read the two time components simultaneously in order to get a clean reading. Otherwise, we may read the timer count just before it turns over and generates an interrupt, then read the BIOS time-of-day count just after the interrupt has occurred and caused the time-of-day count to turn over, with a resulting 54 ms measurement inaccuracy. (The opposite sequence&#151;reading the time-of-day count and then the timer count&#151;can result in a 54 ms inaccuracy in the other direction.)
</P>
<P>The only way to avoid this problem is to stop timer 0, read both the timer and time-of-day counts while the timer is stopped, and then restart the timer. Alas, the gate input to timer 0 isn&#146;t program-controllable in the PC, so there&#146;s no documented way to stop the timer. (The latched read feature we used in Listing 3.1 doesn&#146;t stop the timer; it latches a count, but the timer keeps running.) What should we do?</P>
<P>As it turns out, an undocumented feature of the 8253 makes it possible to stop the timer dead in its tracks. Setting the timer to a new mode and waiting for an initial count to be loaded causes the timer to stop until the count is loaded. Surprisingly, the timer count remains readable and correct while the timer is waiting for the initial load.</P>
<P>In my experience, this approach works beautifully with fully 8253-compatible chips. However, there&#146;s no guarantee that it will always work, since it programs the 8253 in an undocumented way. What&#146;s more, IBM chose not to implement compatibility with this particular 8253 feature in the custom chips used in PS/2 computers. On PS/2 computers, we have no choice but to latch the timer 0 count and then stop the BIOS count (by disabling interrupts) as quickly as possible. We&#146;ll just have to accept the fact that on PS/2 computers we may occasionally get a reading that&#146;s off by 54 ms, and leave it at that.</P>
<P>I&#146;ve set up Listing 3.5 so that it can assemble to either use or not use the undocumented timer-stopping feature, as you please. The <B>PS2</B> equate selects between the two modes of operation. If <B>PS2</B> is 1 (as it is in Listing 3.5), then the latch-and-read method is used; if <B>PS2</B> is 0, then the undocumented timer-stop approach is used. The latch-and-read method will work on all PC-compatible computers, but may occasionally produce results that are incorrect by 54 ms. The timer-stop approach avoids synchronization problems, but doesn&#146;t work on all computers.</P>
<P><B>LISTING 3.5 LZTIMER.ASM</B></P>
<!-- CODE //-->
<PRE>
;
; The long-period Zen timer. (LZTIMER.ASM)
; Uses the 8253 timer and the BIOS time-of-day count to time the
; performance of code that takes less than an hour to execute.
; Because interrupts are left on (in order to allow the timer
; interrupt to be recognized), this is less accurate than the
; precision Zen timer, so it is best used only to time code that takes
; more than about 54 milliseconds to execute (code that the precision
; Zen timer reports overflow on). Resolution is limited by the
; occurrence of timer interrupts.
;
; By Michael Abrash
;
; Externally callable routines:
;
; ZTimerOn: Saves the BIOS time of day count and starts the
; long-period Zen timer.
;
; ZTimerOff: Stops the long-period Zen timer and saves the timer
; count and the BIOS time-of-day count.
;
; ZTimerReport: Prints the time that passed between starting and
; stopping the timer.
;
; Note: If either more than an hour passes or midnight falls between
; calls to ZTimerOn and ZTimerOff, an error is reported. For
; timing code that takes more than a few minutes to execute,
; either the DOS TIME command in a batch file before and after
; execution of the code to time or the use of the DOS
; time-of-day function in place of the long-period Zen timer is
; more than adequate.
;
; Note: The PS/2 version is assembled by setting the symbol PS2 to 1.
; PS2 must be set to 1 on PS/2 computers because the PS/2&#146;s
; timers are not compatible with an undocumented timer-stopping
; feature of the 8253; the alternative timing approach that
; must be used on PS/2 computers leaves a short window
; during which the timer 0 count and the BIOS timer count may
; not be synchronized. You should also set the PS2 symbol to
; 1 if you&#146;re getting erratic or obviously incorrect results.
;
; Note: When PS2 is 0, the code relies on an undocumented 8253
; feature to get more reliable readings. It is possible that
; the 8253 (or whatever chip is emulating the 8253) may be put
; into an undefined or incorrect state when this feature is
; used.
;
; ******************************************************************
; * If your computer displays any hint of erratic behavior *
; * after the long-period Zen timer is used, such as the floppy*
; * drive failing to operate properly, reboot the system, set *
; * PS2 to 1 and leave it that way! *
; ******************************************************************
;
; Note: Each block of code being timed should ideally be run several
; times, with at least two similar readings required to
; establish a true measurement, in order to eliminate any
; variability caused by interrupts.
;
; Note: Interrupts must not be disabled for more than 54 ms at a
; stretch during the timing interval. Because interrupts
; are enabled, keys, mice, and other devices that generate
; interrupts should not be used during the timing interval.
;
; Note: Any extra code running off the timer interrupt (such as
; some memory-resident utilities) will increase the time
; measured by the Zen timer.
;
; Note: These routines can introduce inaccuracies of up to a few
; tenths of a second into the system clock count for each
; code section timed. Consequently, it&#146;s a good idea to
; reboot at the conclusion of timing sessions. (The
; battery-backed clock, if any, is not affected by the Zen
; timer.)
;
; All registers and all flags are preserved by all routines.
;
Code segment word public &#145;CODE&#146;
assume cs: Code, ds:nothing
public ZTimerOn, ZTimerOff, ZTimerReport
;
; Set PS2 to 0 to assemble for use on a fully 8253-compatible
; system; when PS2 is 0, the readings are more reliable if the
; computer supports the undocumented timer-stopping feature,
; but may be badly off if that feature is not supported. In
; fact, timer-stopping may interfere with your computer&#146;s
; overall operation by putting the 8253 into an undefined or
; incorrect state. Use with caution!!!
;
; Set PS2 to 1 to assemble for use on non-8253-compatible
; systems, including PS/2 computers; when PS2 is 1, readings
; may occasionally be off by 54 ms, but the code will work
; properly on all systems.
;
; A setting of 1 is safer and will work on more systems,
; while a setting of 0 produces more reliable results in systems
; which support the undocumented timer-stopping feature of the
; 8253. The choice is yours.
;
PS2 equ1
;
; Base address of the 8253 timer chip.
;
BASE_8253 equ40h
;
; The address of the timer 0 count registers in the 8253.
;
TIMER_0_8253 equBASE_8253 &#43; 0
;
; The address of the mode register in the 8253.
;
MODE_8253 equBASE_8253 &#43; 3
;
; The address of the BIOS timer count variable in the BIOS
; data segment.
;
TIMER_COUNT equ46ch
;
; Macro to emulate a POPF instruction in order to fix the bug in some
; 80286 chips which allows interrupts to occur during a POPF even when
; interrupts remain disabled.
;
MPOPF macro
local p1, p2
jmp short p2
p1: iret ;jump to pushed address &amp pop flags
p2: pushcs ;construct far return address to
call p1 ; the next instruction
endm
;
; Macro to delay briefly to ensure that enough time has elapsed
; between successive I/O accesses so that the device being accessed
; can respond to both accesses even on a very fast PC.
;
DELAY macro
jmp $&#43;2
jmp $&#43;2
jmp $&#43;2
endm
StartBIOSCountLow dw ? ;BIOS count low word at the
; start of the timing period
StartBIOSCountHigh dw ? ;BIOS count high word at the
; start of the timing period
EndBIOSCountLow dw ? ;BIOS count low word at the
; end of the timing period
EndBIOSCountHigh dw ? ;BIOS count high word at the
; end of the timing period
EndTimedCount dw ? ;timer 0 count at the end of
; the timing period
ReferenceCount dw ? ;number of counts required to
; execute timer overhead code
;
; String printed to report results.
;
OutputStr labelbyte
db 0dh, 0ah, &#145;Timed count: &#145;
TimedCountStr db10 dup (?)
db&#146; microseconds&#146;, 0dh, 0ah
db &#145;$&#146;
;
; Temporary storage for timed count as it&#146;s divided down by powers
; of ten when converting from doubleword binary to ASCII.
;
CurrentCountLow dw ?
CurrentCountHigh dw ?
;
; Powers of ten table used to perform division by 10 when doing
; doubleword conversion from binary to ASCII.
;
PowersOfTenlabelword
dd 1
dd 10
dd 100
dd 1000
dd 10000
dd 100000
dd 1000000
dd 10000000
dd 100000000
dd 1000000000
PowersOfTenEnd labelword
;
; String printed to report that the high word of the BIOS count
; changed while timing (an hour elapsed or midnight was crossed),
; and so the count is invalid and the test needs to be rerun.
;
TurnOverStrlabelbyte
db 0dh, 0ah
db &#145;****************************************************&#146;
db 0dh, 0ah
db&#146;* Either midnight passed or an hour or more passed *&#146;
db 0dh, 0ah
db&#146;* while timing was in progress. If the former was *&#146;
db 0dh, 0ah
db&#146;* the case, please rerun the test; if the latter *&#146;
db 0dh, 0ah
db&#146;* was the case, the test code takes too long to *&#146;
db 0dh, 0ah
db&#146;* run to be timed by the long-period Zen timer. *&#146;
db 0dh, 0ah
db &#145;* Suggestions: use the DOS TIME command, the DOS *&#146;
db 0dh, 0ah
db &#145;* time function, or a watch. *&#146;
db 0dh, 0ah
db &#145;****************************************************&#146;
db 0dh, 0ah
db&#146;$&#146;
;********************************************************************
;* Routine called to start timing. *
;********************************************************************
ZTimerOn proc near
;
; Save the context of the program being timed.
;
push ax
pus hf
;
; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting. Also stops
; timer 0 until the timer count is loaded, except on PS/2
; computers.
;
mov al,00110100b ;mode 2
out MODE_8253,al
;
; Set the timer count to 0, so we know we won&#146;t get another
; timer interrupt right away.
; Note: this introduces an inaccuracy of up to 54 ms in the system
; clock count each time it is executed.
;
DELAY
subal,al
outTIMER_0_8253,al ;lsb
DELAY
outTIMER_0_8253,al ;msb
;
; In case interrupts are disabled, enable interrupts briefly to allow
; the interrupt generated when switching from mode 3 to mode 2 to be
; recognized. Interrupts must be enabled for at least 210 ns to allow
; time for that interrupt to occur. Here, 10 jumps are used for the
; delay to ensure that the delay time will be more than long enough
; even on a very fast PC.
;
pushf
sti
rept 10
jmp $&#43;2
endm
MPOPF
;
; Store the timing start BIOS count.
; (Since the timer count was just set to 0, the BIOS count will
; stay the same for the next 54 ms, so we don&#146;t need to disable
; interrupts in order to avoid getting a half-changed count.)
;
push ds
subax, ax
movds, ax
movax, ds:[TIMER_COUNT&#43;2]
movcs: [StartBIOSCountHigh],ax
movax, ds:[TIMER_COUNT]
movcs: [StartBIOSCountLow],ax
pop ds
;
; Set the timer count to 0 again to start the timing interval.
;
mov al,00110100b ;set up to load initial
out MODE_8253,al ; timer count
DELAY
subal, al
out TIMER_0_8253,al; load count lsb
DELAY
out TIMER_0_8253,al; load count msb
;
; Restore the context of the program being timed and return to it.
;
MPOPF
popax
ret
ZTimerOnendp
;********************************************************************
;* Routine called to stop timing and get count. *
;********************************************************************
ZTimerOff procnear
;
; Save the context of the program being timed.
;
pushf
pushax
pushcx
;
; In case interrupts are disabled, enable interrupts briefly to allow
; any pending timer interrupt to be handled. Interrupts must be
; enabled for at least 210 ns to allow time for that interrupt to
; occur. Here, 10 jumps are used for the delay to ensure that the
; delay time will be more than long enough even on a very fast PC.
;
sti
rept 10
jmp $&#43;2
endm
;
; Latch the timer count.
;
if PS2
mov al,00000000b
out MODE_8253,al ;latch timer 0 count
;
; This is where a one-instruction-long window exists on the PS/2.
; The timer count and the BIOS count can lose synchronization;
; since the timer keeps counting after it&#146;s latched, it can turn
; over right after it&#146;s latched and cause the BIOS count to turn
; over before interrupts are disabled, leaving us with the timer
; count from before the timer turned over coupled with the BIOS
; count from after the timer turned over. The result is a count
; that&#146;s 54 ms too long.
;
else
;
; Set timer 0 to mode 2 (divide-by-N), waiting for a 2-byte count
; load, which stops timer 0 until the count is loaded. (Only works
; on fully 8253-compatible chips.)
;
mov al,00110100b; mode 2
out MODE_8253,al
DELAY
mov al,00000000b ;latch timer 0 count
out MODE_8253,al
endif
cli ;stop the BIOS count
;
; Read the BIOS count. (Since interrupts are disabled, the BIOS
; count won&#146;t change.)
;
push ds
sub ax,ax
mov ds,ax
mov ax,ds:[TIMER_COUNT&#43;2]
mov cs:[EndBIOSCountHigh],ax
mov ax,ds:[TIMER_COUNT]
mov cs:[EndBIOSCountLow],ax
pop ds
;
; Read the timer count and save it.
;
in al,TIMER_0_8253 ;lsb
DELAY
mov ah,al
in al,TIMER_0_8253 ;msb
xchg ah,al
neg ax ;convert from countdown
; remaining to elapsed
; count
mov cs:[EndTimedCount],ax
;
; Restart timer 0, which is still waiting for an initial count
; to be loaded.
;
ife PS2
DELAY
mov al,00110100b ;mode 2, waiting to load a
; 2-byte count
out MODE_8253,al
DELAY
sub al,al
out TIMER_0_8253,al ;lsb
DELAY
mov al,ah
out TIMER_0_8253,al ;msb
DELAY
endif
sti;let the BIOS count continue
;
; Time a zero-length code fragment, to get a reference for how
; much overhead this routine has. Time it 16 times and average it,
; for accuracy, rounding the result.
;
mov cs:[ReferenceCount],0
mov cx,16
cli ;interrupts off to allow a
; precise reference count
RefLoop:
call ReferenceZTimerOn
call ReferenceZTimerOff
loop RefLoop
sti
add cs:[ReferenceCount],8; total &#43; (0.5 * 16)
mov cl,4
shr cs:[ReferenceCount],cl;(total) / 16 &#43; 0.5
;
; Restore the context of the program being timed and return to it.
;
popcx
popax
MPOPF
ret
ZTimerOff endp
;
; Called by ZTimerOff to start the timer for overhead measurements.
;
ReferenceZTimerOnprocnear
;
; Save the context of the program being timed.
;
pushax
pushf
;
; Set timer 0 of the 8253 to mode 2 (divide-by-N), to cause
; linear counting rather than count-by-two counting.
;
mov al,00110100b ;mode 2
out MODE_8253,al
;
; Set the timer count to 0.
;
DELAY
sub al,al
out TIMER_0_8253,al ;lsb
DELAY
out TIMER_0_8253,al ;msb
;
; Restore the context of the program being timed and return to it.
;
MPOPF
popax
ret
ReferenceZTimerOnendp
;
; Called by ZTimerOff to stop the timer and add the result to
; ReferenceCount for overhead measurements. Doesn&#146;t need to look
; at the BIOS count because timing a zero-length code fragment
; isn&#146;t going to take anywhere near 54 ms.
;
ReferenceZTimerOff procnear
;
; Save the context of the program being timed.
;
pushf
pushax
pushcx
;
; Match the interrupt-window delay in ZTimerOff.
;
sti
rept10
jmp$&#43;2
endm
mov al,00000000b
out MODE_8253,al ;latch timer
;
; Read the count and save it.
;
DELAY
in al,TIMER_0_8253 ;lsb
DELAY
mov ah,al
in al,TIMER_0_8253 ;msb
xchg ah,al
neg ax ;convert from countdown
; remaining to elapsed
; count
add cs:[ReferenceCount],ax
;
; Restore the context and return.
;
popcx
popax
MPOPF
ret
ReferenceZTimerOff endp
;********************************************************************
;* Routine called to report timing results. *
;********************************************************************
ZTimerReportprocnear
pushf
push ax
push bx
push cx
push dx
push si
push di
push ds
;
push cs ;DOS functions require that DS point
pop ds ; to text to be displayed on the screen
assume ds :Code
;
; See if midnight or more than an hour passed during timing. If so,
; notify the user.
;
mov ax,[StartBIOSCountHigh]
cmp ax,[EndBIOSCountHigh]
jz CalcBIOSTime ;hour count didn&#146;t change,
; so everything&#146;s fine
inc ax
cmp ax,[EndBIOSCountHigh]
jnz TestTooLong ;midnight or two hour
; boundaries passed, so the
; results are no good
mov ax,[EndBIOSCountLow]
cmp ax,[StartBIOSCountLow]
jb CalcBIOSTime ;a single hour boundary
; passed--that&#146;s OK, so long as
; the total time wasn&#146;t more
; than an hour
;
; Over an hour elapsed or midnight passed during timing, which
; renders the results invalid. Notify the user. This misses the
; case where a multiple of 24 hours has passed, but we&#146;ll rely
; on the perspicacity of the user to detect that case.
;
TestTooLong:
mov ah,9
mov dx,offset TurnOverStr
int 21h
jmp short ZTimerReportDone
;
; Convert the BIOS time to microseconds.
;
CalcBIOSTime:
mov ax,[EndBIOSCountLow]
sub ax,[StartBIOSCountLow]
mov dx,54925 ;number of microseconds each
; BIOS count represents
mul dx
mov bx,ax ;set aside BIOS count in
mov cx,dx ; microseconds
;
; Convert timer count to microseconds.
;
mov ax,[EndTimedCount]
mov si,8381
mul si
mov si,10000
div si ;* .8381 = * 8381 / 10000
;
; Add timer and BIOS counts together to get an overall time in
; microseconds.
;
add bx,ax
adc cx,0
;
; Subtract the timer overhead and save the result.
;
mov ax,[ReferenceCount]
mov si,8381 ;convert the reference count
mul si ; to microseconds
mov si,10000
div si;* .8381 = * 8381 / 10000
sub bx,ax
sbb cx,0
mov [CurrentCountLow],bx
mov [CurrentCountHigh],cx
;
; Convert the result to an ASCII string by trial subtractions of
; powers of 10.
;
mov di,offset PowersOfTenEnd - offset PowersOfTen - 4
mov si,offset TimedCountStr
CTSNextDigit:
mov bl,&#146;0&#146;
CTSLoop:
mov ax,[CurrentCountLow]
mov dx,[CurrentCountHigh]
sub ax,PowersOfTen[di]
sbb dx,PowersOfTen[di&#43;2]
jc CTSNextPowerDown
inc bl
mov [CurrentCountLow],ax
mov [CurrentCountHigh],dx
jmp CTSLoop
CTSNextPowerDown:
mov [si],bl
inc si
sub di,4
jns CTSNextDigit
;
;
; Print the results.
;
mov ah,9
mov dx,offset OutputStr
int 21h
;
ZTimerReportDone:
pop ds
pop di
pop si
pop dx
pop cx
pop bx
pop ax
MPOPF
ret
ZTimerReport endp
Code ends
end
</PRE>
<!-- END CODE //-->
<P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-06.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-08.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

137
ch03/03-08.html Normal file
View file

@ -0,0 +1,137 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Assume Nothing</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=03//-->
<!--PAGES=065-067//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-07.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-09.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>Moreover, because it uses an undocumented feature, the timer-stop approach could conceivably cause erratic 8253 operation, which could in turn seriously affect your computer&#146;s operation until the next reboot. In non-8253-compatible systems, I&#146;ve observed not only wildly incorrect timing results, but also failure of a diskette drive to operate properly after the long-period Zen timer with <B>PS2</B> set to 0 has run, so be alert for signs of trouble if you do set <B>PS2</B> to 0.</P>
<P>Rebooting should clear up any timer-related problems of the sort described above. (This gives us another reason to reboot at the end of each code-timing session.) You should <I>immediately</I> reboot and set the <B>PS2</B> equate to 1 if you get erratic or obviously incorrect results with the long-period Zen timer when <B>PS2</B> is set to 0. If you want to set <B>PS2</B> to 0, it would be a good idea to time a few of the listings in this book with <B>PS2</B> set first to 1 and then to 0, to make sure that the results match. If they&#146;re consistently different, you should set <B>PS2</B> to 1.</P>
<P>While the the non-PS/2 version is more dangerous than the PS/2 version, it also produces more accurate results when it does work. If you have a non-PS/2 PC-compatible computer, the choice between the two timing approaches is yours.</P>
<P>If you do leave the <B>PS2</B> equate at 1 in Listing 3.5, you should repeat each code-timing run several times before relying on the results to be accurate to more than 54 ms, since variations may result from the possible lack of synchronization between the timer 0 count and the BIOS time-of-day count. In fact, it&#146;s a good idea to time code more than once no matter which version of the long-period Zen timer you&#146;re using, since interrupts, which must be enabled in order for the long-period timer to work properly, may occur at any time and can alter execution time substantially.</P>
<P>Finally, please note that the <I>precision</I> Zen timer works perfectly well on both PS/2 and non-PS/2 computers. The PS/2 and 8253 considerations we&#146;ve just discussed apply <I>only</I> to the longZen timer.</P>
<H3><A NAME="Heading14"></A><FONT COLOR="#000077">Example Use of the Long-Period Zen Timer</FONT></H3>
<P>The long-period Zen timer has exactly the same calling interface as the precision Zen timer, and can be used in place of the precision Zen timer simply by linking it to the code to be timed in place of linking the precision timer code. Whenever the precision Zen timer informs you that the code being timed takes too long for the precision timer to handle, all you have to do is link in the long-period timer instead.
</P>
<P>Listing 3.6 shows a test-bed program for the long-period Zen timer. While this program is similar to Listing 3.2, it&#146;s worth noting that Listing 3.6 waits for a few seconds before calling <B>ZTimerOn</B>, thereby allowing any pending keyboard interrupts to be processed. Since interrupts must be left on in order to time periods longer than 54 ms, the interrupts generated by keystrokes (including the upstroke of the Enter key press that starts the program)&#151;or any other interrupts, for that matter&#151;could incorrectly inflate the time recorded by the long-period Zen timer. In light of this, resist the temptation to type ahead, move the mouse, or the like while the long-period Zen timer is timing.</P>
<P><B>LISTING 3.6 LZTEST.ASM</B></P>
<!-- CODE //-->
<PRE>
; Program to measure performance of code that takes longer than
; 54 ms to execute. (LZTEST.ASM)
;
; Link with LZTIMER.ASM (Listing 3.5). LZTIME.BAT (Listing 3.7)
; can be used to assemble and link both files. Code to be
; measured must be in the file TESTCODE; Listing 3.8 shows
; a sample file (LST3-8.ASM) which should be named TESTCODE.
;
; By Michael Abrash
;
mystack segment para stack &#145;STACK&#146;
db 512 dup(?)
mystack ends
;
Code segment para public &#145;CODE&#146;
assume cs:Code, ds:Code
extrn ZTimerOn:near, ZTimerOff:near, ZTimerReport:near
Startproc near
push cs
pop ds ;point DS to the code segment,
; so data as well as code can easily
; be included in TESTCODE
;
; Delay for 6-7 seconds, to let the Enter keystroke that started the
; program come back up.
;
mov ah,2ch
int 21h ;get the current time
mov bh,dh ;set the current time aside
DelayLoop:
mov ah,2ch
push bx ;preserve start time
int 21h ;get time
pop bx ;retrieve start time
cmp dh,bh ;is the new seconds count less than
; the start seconds count?
jnb CheckDelayTime ;no
add dh,60 ;yes, a minute must have turned over,
; so add one minute
CheckDelayTime:
sub dh,bh ;get time that&#146;s passed
cmp dh,7 ;has it been more than 6 seconds yet?
jb DelayLoop ;not yet
;
include TESTCODE ;code to be measured, including calls
; to ZTimerOn and ZTimerOff
;
; Display the results.
;
call ZTimerReport
;
; Terminate the program.
;
mov ah,4ch
int 21h
Start endp
Code ends
end Start
</PRE>
<!-- END CODE //-->
<P>As with the precision Zen timer, the program in Listing 3.6 is used by naming the file containing the code to be timed TESTCODE, then assembling both Listing 3.6 and Listing 3.5 with MASM or TASM and linking the two files together by way of the Microsoft or Borland linker. Listing 3.7 shows a batch file, named LZTIME.BAT, which does all of the above, generating and running the executable file LZTEST.EXE. LZTIME.BAT assumes that the file LZTIMER.ASM contains Listing 3.5 and the file LZTEST.ASM contains Listing 3.6.
</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-07.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-09.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

181
ch03/03-09.html Normal file
View file

@ -0,0 +1,181 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Assume Nothing</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=03//-->
<!--PAGES=067-070//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-08.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-10.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P><B>LISTING 3.7 LZTIME.BAT</B></P>
<!-- CODE //-->
<PRE>
echo off
rem
rem *** Listing 3.7 ***
rem
rem ***************************************************************
rem * Batch file LZTIME.BAT, which builds and runs the *
rem * long-period Zen timer program LZTEST.EXE to time the code *
rem * named as the command-line parameter. Listing 3.5 must be *
rem * named LZTIMER.ASM, and Listing 3.6 must be named *
rem * LZTEST.ASM. To time the code in LST3-8, you&#146;d type the *
rem * DOS command: *
rem * *
rem * lztime lst3-8 *
rem * *
rem * Note that MASM and LINK must be in the current directory or *
rem * on the current path in order for this batch file to work. *
rem * *
rem * This batch file can be speeded up by assembling LZTIMER.ASM *
rem * once, then removing the lines: *
rem * *
rem * masm lztimer; *
rem * if errorlevel 1 goto errorend *
rem * *
rem * from this file. *
rem * *
rem * By Michael Abrash *
rem ***************************************************************
rem
rem Make sure a file to test was specified.
rem
if not x%1==x goto ckexist
echo ***************************************************************
echo * Please specify a file to test. *
echo ***************************************************************
goto end
rem
rem Make sure the file exists.
rem
:ckexist
if exist %1 goto docopy
echo ***************************************************************
echo * The specified file, &#147;%1,&#148; doesn&#146;t exist. *
echo ***************************************************************
goto end
rem
rem copy the file to measure to TESTCODE.
:docopy
copy %1 testcode
masm lztest;
if errorlevel 1 goto errorend
masm lztimer;
if errorlevel 1 goto errorend
link lztest&#43;lztimer;
if errorlevel 1 goto errorend
lztest
goto end
:errorend
echo ***************************************************************
echo * An error occurred while building the long-period Zen timer. *
echo ***************************************************************
:end
</PRE>
<!-- END CODE //-->
<P>Listing 3.8 shows sample code that can be timed with the test-bed program of Listing 3.6. Listing 3.8 measures the time required to execute 20,000 loads of AL from memory, a length of time too long for the precision Zen timer to handle on the 8088.
</P>
<P><B>LISTING 3.8 LST3-8.ASM</B></P>
<!-- CODE //-->
<PRE>
;
; Measures the performance of 20,000 loads of AL from
; memory. (Use by renaming to TESTCODE, which is
; included by LZTEST.ASM (Listing 3.6). LZTIME.BAT
; (Listing 3.7) does this, along with all assembly
; and linking.)
;
; Note: takes about ten minutes to assemble on a slow PC if
;you are using MASM
;
jmpSkip;jump around defined data
;
MemVardb?
;
Skip:
;
; Start timing.
;
callZTimerOn
;
rept20000
moval,[MemVar]
endm
;
; Stop timing.
;
callZTimerOff
</PRE>
<!-- END CODE //-->
<P>When LZTIME.BAT is run on a PC with the following command line (assuming the code in Listing 3.8 is the file LST3-8.ASM)
</P>
<!-- CODE SNIP //-->
<PRE>
lztime lst3-8.asm
</PRE>
<!-- END CODE SNIP //-->
<P>the result is 72,544 &#181;s, or about 3.63 &#181;s per load of AL from memory. This is just slightly longer than the time per load of AL measured by the precision Zen timer, as we would expect given that interrupts are left enabled by the long-period Zen timer. The extra fraction of a microsecond measured per <B>MOV</B> reflects the time required to execute the BIOS code that handles the 18.2 timer interrupts that occur each second.</P>
<P>Note that the command can take as much as 10 minutes to finish on a slow PC if you are using MASM, with most of that time spent assembling Listing 3.8. Why? Because MASM is notoriously slow at assembling <B>REPT</B> blocks, and the block in Listing 3.8 is repeated 20,000 times.</P>
<H3><A NAME="Heading15"></A><FONT COLOR="#000077">Using the Zen Timer from C</FONT></H3>
<P>The Zen timer can be used to measure code performance when programming in C&#151;but not right out of the box. As presented earlier, the timer is designed to be called from assembly language; some relatively minor modifications are required before the <B>ZTimerOn</B> (start timer), <B>ZTimerOff</B> (stop timer), and <B>ZTimerReport</B> (display timing results) routines can be called from C. There are two separate cases to be dealt with here: small code model and large; I&#146;ll tackle the simpler one, the small code model, first.</P>
<P>Altering the Zen timer for linking to a small code model C program involves the following steps: <B> C</B> hange <B>ZTimerOn</B> to <B>_ZTimerOn</B>, change <B>ZTimerOff</B> to <B>_ZTimerOff</B>, change <B>ZTimerReport</B> to <B>_ZTimerReport</B>, and change <B>Code</B> to <B>_TEXT</B> . Figure 3.2 shows the line numbers and new states of all lines from Listing 3.1 that must be changed. These changes convert the code to use C-style external label names and the small model C code segment. (In C<SMALL>&#43;&#43;</SMALL>, use the &#147;C&#148; specifier, as in</P>
<!-- CODE SNIP //-->
<PRE>
extern &#147;C&#148; ZTimerOn(void);
</PRE>
<!-- END CODE SNIP //-->
<P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-08.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="03-10.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

113
ch03/03-10.html Normal file
View file

@ -0,0 +1,113 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Assume Nothing</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=03//-->
<!--PAGES=070-073//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-09.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch04/04-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>when declaring the timer routines <B>extern</B>, so that name-mangling doesn&#146;t occur, and the linker can find the routines&#146; C-style names.)</P>
<P>That&#146;s all it takes; after doing this, you&#146;ll be able to use the Zen timer from C, as, for example, in:</P>
<!-- CODE SNIP //-->
<PRE>
ZTimerOn():
for (i=0, x=0; i&lt100; i&#43;&#43;)
x &#43;= i;
ZTimerOff();
ZTimerReport();
</PRE>
<!-- END CODE SNIP //-->
<P>(I&#146;m talking about the precision timer here. The long-period timer&#151;Listing 3.5&#151;requires the same modifications, but to different lines.)
</P>
<P><A NAME="Fig2"><!-- </A><A HREF="javascript:displayWindow('images/03-02.jpg',410,189 )"> --><IMG SRC="images/03-02.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/03-02.jpg',410,189)"> --><FONT COLOR="#000077"><B>Figure 3.2</B></FONT></A>&nbsp;&nbsp;<I>Changes for use with small code model C.</I>
</P>
<P>Altering the Zen timer for use in C&#146;s large code model is a tad more complex, because in addition to the above changes, all functions, including the internal reference timing routines that are used to calculate overhead so it can be subtracted out, must be converted to far. Figure 3.3 shows the line numbers and new states of all lines from Listing 3.1 that must be changed in order to call the Zen timer from large code model C. Again, the line numbers are specific to the precision timer, but the long-period timer is very similar.
</P>
<P>The full listings for the C-callable Zen timers are presented in Chapter K on the companion CD-ROM.</P>
<H4 ALIGN="LEFT"><A NAME="Heading16"></A><FONT COLOR="#000077">Watch Out for Optimizing Assemblers!</FONT></H4>
<P>One important safety tip when modifying the Zen timer for use with large code model C code: Watch out for optimizing assemblers! TASM actually replaces
</P>
<!-- CODE SNIP //-->
<PRE>
call far ptr ReferenceZTimerOn
</PRE>
<!-- END CODE SNIP //-->
<P>with
</P>
<!-- CODE SNIP //-->
<PRE>
push cs
call near ptr ReferenceZTimerOn
</PRE>
<!-- END CODE SNIP //-->
<P>(and likewise for <B>ReferenceZTimerOff</B> ), which works because <B>ReferenceZTimerOn</B> is in the same segment as the calling code. This is normally a great optimization, being both smaller and faster than a far call. However, it&#146;s not so great for the Zen</P>
<P><A NAME="Fig3"><!-- </A><A HREF="javascript:displayWindow('images/03-03.jpg',413,221 )"> --><IMG SRC="images/03-03.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/03-03.jpg',413,221)"> --><FONT COLOR="#000077"><B>Figure 3.3</B></FONT></A>&nbsp;&nbsp;<I>Changes for use with large code model C.</I>
</P>
<P>timer, because our purpose in calling the reference timing code is to determine exactly how much time is taken by overhead code&#151;including the far calls to <B>ZTimerOn</B> and <B>ZTimerOf</B>f! By converting the far calls to push/near call pairs within the Zen timer module, TASM makes it impossible to emulate exactly the overhead of the Zen timer, and makes timings slightly (about 16 cycles on a 386) less accurate.</P>
<P>What&#146;s the solution? Put the <B>NOSMART</B> directive at the start of the Zen timer code. This directive instructs TASM to turn off all optimizations, including converting far calls to push/near call pairs. By the way, there is, to the best of my knowledge, no such problem with MASM up through version 5.10A.</P>
<P>In my mind, the whole business of optimizing assemblers is a mixed blessing. In general, it&#146;s nice to have the assembler shortening jumps and selecting sign-extended forms of instructions for you. On the other hand, the benefits of tricks like substituting push/near call pairs for far calls are relatively small, and those tricks can get in the way when complete control is needed. Sure, complete control is needed very rarely, but when it is, optimizing assemblers can cause subtle problems; I discovered TASM&#146;s alteration of far calls only because I happened to view the code in the debugger, and you might want to do the same if you&#146;re using a recent version of MASM.</P>
<P>I&#146;ve tested the changes shown in Figures 3.2 and 3.3 with TASM and Borland C<SMALL>&#43;&#43;</SMALL> 4.0, and also with the latest MASM and Microsoft C/C<SMALL>&#43;&#43;</SMALL> compiler.</P>
<H4 ALIGN="LEFT"><A NAME="Heading17"></A><FONT COLOR="#000077">Further Reading</FONT></H4>
<P>For those of you who wish to pursue the mechanics of code measurement further, one good article about measuring code performance with the 8253 timer is &#147;Programming Insight: High-Performance Software Analysis on the IBM PC,&#148; by Byron Sheppard, which appeared in the January, 1987 issue of <I>Byte</I>. For complete if somewhat cryptic information on the 8253 timer itself, I refer you to Intel&#146;s <I>Microsystem Components Handbook</I>, which is also a useful reference for a number of other PC components, including the 8259 Programmable Interrupt Controller and the 8237 DMA Controller. For details about the way the 8253 is used in the PC, as well as a great deal of additional information about the PC&#146;s hardware and BIOS resources, I suggest you consult IBM&#146;s series of technical reference manuals for the PC, XT, AT, Model 30, and microchannel computers, such as the Models 50, 60, and 80.</P>
<P>For our purposes, however, it&#146;s not critical that you understand exactly how the Zen timer works. All you really need to know is what the Zen timer can do and how to use it, and we&#146;ve accomplished that in this chapter.</P>
<H4 ALIGN="LEFT"><A NAME="Heading18"></A><FONT COLOR="#000077">Armed with the Zen Timer, Onward and Upward</FONT></H4>
<P>The Zen timer is not perfect. For one thing, the finest resolution to which it can measure an interval is at best about 1&#181;s, a period of time in which a 66 MHz Pentium computer can execute as many as 132 instructions (although an 8088-based PC would be hard-pressed to manage two instructions in a microsecond). Another problem is that the timing code itself interferes with the state of the prefetch queue and processor cache at the start of the code being timed, because the timing code is not necessarily fetched and does not necessarily access memory in exactly the same time sequence as the code immediately preceding the code under measurement normally does. This prefetch effect can introduce as much as 3 to 4 &#181; of inaccuracy. Similarly, the state of the prefetch queue at the end of the code being timed affects how long the code that stops the timer takes to execute. Consequently, the Zen timer tends to be more accurate for longer code sequences, since the relative magnitude of the inaccuracy introduced by the Zen timer becomes less over longer periods.
</P>
<P>Imperfections notwithstanding, the Zen timer is a good tool for exploring C code and x86 family assembly language, and it&#146;s a tool we&#146;ll use frequently for the remainder of this book.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="03-09.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch04/04-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

BIN
ch03/images/03-01.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
ch03/images/03-01i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch03/images/03-02.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
ch03/images/03-03.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

93
ch04/04-01.html Normal file
View file

@ -0,0 +1,93 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: In the Lair of the Cycle-Eaters</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=04//-->
<!--PAGES=075-080//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch03/03-10.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H2><A NAME="Heading1"></A><FONT COLOR="#000077">Chapter 4<BR>In the Lair of the Cycle-Eaters
</FONT></H2>
<H3><A NAME="Heading2"></A><FONT COLOR="#000077">How the PC Hardware Devours Code Performance</FONT></H3>
<P>This chapter, adapted from my earlier book, <I>Zen of Assembly Language</I> located on the companion CD-ROM, goes right to the heart of my philosophy of optimization: Understand where the time really goes when your code runs. That may sound ridiculously simple, but, as this chapter makes clear, it turns out to be a challenging task indeed, one that at times verges on black magic. This chapter is a long-time favorite of mine because it was the first&#151;and to a large extent only&#151;work that I know of that discussed this material, thereby introducing a generation of PC programmers to pedal-to-the-metal optimization.</P>
<P>This chapter focuses almost entirely on the first popular x86-family processor, the 8088. Some of the specific features and results that I cite in this chapter are no longer applicable to modern x86-family processors such as the 486 and Pentium, as I&#146;ll point out later on when we discuss those processors. Nonetheless, the overall theme of this chapter&#151;that understanding dimly-seen and poorly-documented code gremlins called cycle-eaters that lurk in your system is essential to performance programming&#151;is every bit as valid today. Also, later chapters often refer back to the basic cycle-eaters described in this chapter, so this chapter is the foundation for the discussions of x86-family optimization to come. What&#146;s more, the Zen timer remains an excellent tool with which to flush out and examine cycle-eaters, as we&#146;ll see in later chapters, and this chapter is as good an illustration of how to use the Zen timer as you&#146;re likely to find.</P>
<P>So, don&#146;t take either the absolute or the relative execution times presented in this chapter as gospel for newer processors, and read on to later chapters to see how the cycle-eaters and optimization rules have changed over time, but do take the time to at least skim through this chapter to give yourself a good start on the material in the rest of this book.</P>
<H3><A NAME="Heading3"></A><FONT COLOR="#000077">Cycle-Eaters</FONT></H3>
<P>Programming has many levels, ranging from the familiar (high-level languages, DOS calls, and the like) down to the esoteric things that lie on the shadowy edge of hardware-land. I call these <I>cycle-eaters</I> because, like the monsters in a bad 50s horror movie, they lurk in those shadows, taking their share of your program&#146;s performance without regard to the forces of goodness or the U.S. Army. In this chapter, we&#146;re going to jump right in at the lowest level by examining the cycle-eaters that live beneath the programming interface; that is, beneath your application, DOS, and BIOS&#151;in fact, beneath the instruction set itself.</P>
<P>Why start at the lowest level? Simply because cycle-eaters affect the performance of all assembler code, and yet are almost unknown to most programmers. A full understanding of code optimization requires an understanding of cycle-eaters and their implications. That&#146;s no simple task, and in fact it is in precisely that area that most books and articles about assembly programming fall short.</P>
<P>Nearly all literature on assembly programming discusses only the programming interface: the instruction set, the registers, the flags, and the BIOS and DOS calls. Those topics cover the functionality of assembly programs most thoroughly&#151;but it&#146;s performance above all else that we&#146;re after. No one ever tells you about the raw stuff of performance, which lies <I>beneath</I> the programming interface, in the dimly-seen realm&#151;populated by instruction prefetching, dynamic RAM refresh, and wait states&#151;where software meets hardware. This area is the domain of hardware engineers, and is almost never discussed as it relates to code performance. And yet it is only by understanding the mechanisms operating at this level that we can fully understand and properly improve the performance of our code.</P>
<P>Which brings us to cycle-eaters.</P>
<H3><A NAME="Heading4"></A><FONT COLOR="#000077">The Nature of Cycle-Eaters</FONT></H3>
<P>Cycle-eaters are gremlins that live on the bus or in peripherals (and sometimes within the CPU itself), slowing the performance of PC code so that it doesn&#146;t execute at full speed. Most cycle-eaters (and all of those haunting the older Intel processors) live outside the CPU&#146;s Execution Unit, where they can <I>only</I> affect the CPU when the CPU performs a bus access (a memory or I/O read or write). Once your code and data are already inside the CPU, those cycle-eaters can no longer be a problem. Only on the 486 and Pentium CPUs will you find cycle-eaters inside the chip, as we&#146;ll see in later chapters.</P>
<P>The nature and severity of the cycle-eaters vary enormously from processor to processor, and (especially) from memory architecture to memory architecture. In order to understand them all, we need first to understand the simplest among them, those that haunted the original 8088-based IBM PC. Later on in this book, I&#146;ll be better able to explain the newer generation of cycle-eaters in terms of those ancestral cycle-eaters&#151;but we have to get the groundwork down first.</P>
<H4 ALIGN="LEFT"><A NAME="Heading5"></A><FONT COLOR="#000077">The 8088&#146;s Ancestral Cycle-Eaters</FONT></H4>
<P>Internally, the 8088 is a 16-bit processor, capable of running at full speed at all times&#151;unless external data is required. External data must traverse the 8088&#146;s external data bus and the PC&#146;s data bus one byte at a time to and from peripherals, with cycle-eaters lurking along every step of the way. What&#146;s more, external data includes not only memory operands <I>but also instruction bytes,</I> so even instructions with no memory operands can suffer from cycle-eaters. Since some of the 8088&#146;s fastest instructions are register-only instructions, that&#146;s important indeed.</P>
<P>The major cycle-eaters are:</P>
<DL>
<DD><B>&#149;</B>&nbsp;&nbsp;The 8088&#146;s 8-bit external data bus.
<DD><B>&#149;</B>&nbsp;&nbsp;The prefetch queue.
<DD><B>&#149;</B>&nbsp;&nbsp;Dynamic RAM refresh.
<DD><B>&#149;</B>&nbsp;&nbsp;Wait states, notably display memory wait states and, in the AT and 80386 computers, system memory wait states.
</DL>
<P>The locations of these cycle-eaters in the primordial 8088-based PC are shown in Figure 4.1. We&#146;ll cover each of the cycle-eaters in turn in this chapter. The material won&#146;t be easy since cycle-eaters are among the most subtle aspects of assembly programming. By the same token, however, this will be one of the most important and rewarding chapters in this book. Don&#146;t worry if you don&#146;t catch everything in this chapter, but do read it all even if the going gets a bit tough. Cycle-eaters play a key role in later chapters, so some familiarity with them is highly desirable.
</P>
<H3><A NAME="Heading6"></A><FONT COLOR="#000077">The 8-Bit Bus Cycle-Eater</FONT></H3>
<P><I>Look! Down on the motherboard! It&#146;s a 16-bit processor! It&#146;s an 8-bit processor! It&#146;s...</I></P>
<P>...an 8088!</P>
<P>Fans of the 8088 call it a 16-bit processor. Fans of other 16-bit processors call the 8088 an 8-bit processor. The truth of the matter is that the 8088 is a 16-bit processor that often performs like an 8-bit processor.</P>
<P>The 8088 is internally a full 16-bit processor, equivalent to an 8086. (In fact, the 8086 is identical to the 8088, except that it has a full 16-bit bus. The 8088 is basically the poor man&#146;s 8086, because it allows a cheaper&#151;albeit slower&#151;system to be built, thanks to the half-sized bus.) In terms of the instruction set, the 8088 is clearly a 16-bit processor, capable of performing any given 16-bit operation&#151;addition, subtraction, even multiplication or division&#151;with a single instruction. Externally, however, the 8088 is unequivocally an 8-bit processor, since the external data bus is only 8 bits wide. In other words, the programming interface is 16 bits wide, but the hardware interface is only 8 bits wide, as shown in Figure 4.2. The result of this mismatch is simple: Word-sized data can be transferred between the 8088 and memory or peripherals at only one-half the maximum rate of the 8086, which is to say one-half the maximum rate for which the Execution Unit of the 8088 was designed.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch03/03-10.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

142
ch04/04-02.html Normal file
View file

@ -0,0 +1,142 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: In the Lair of the Cycle-Eaters</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=04//-->
<!--PAGES=080-084//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P><A NAME="Fig1"><!-- </A><A HREF="javascript:displayWindow('images/04-01.jpg',411,379 )"> --><IMG SRC="images/04-01.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/04-01.jpg',411,379)"> --><FONT COLOR="#000077"><B>Figure 4.1</B></FONT></A>&nbsp;&nbsp;<I>The location of the major cycle-eaters in the IBM PC.</I>
</P>
<P><A NAME="Fig2"><!-- </A><A HREF="javascript:displayWindow('images/04-02.jpg',418,236 )"> --><IMG SRC="images/04-02.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/04-02.jpg',418,236)"> --><FONT COLOR="#000077"><B>Figure 4.2</B></FONT></A>&nbsp;&nbsp;<I>Internal data bus widths of the 8088.</I>
</P>
<P>As shown in Figure 4.1, the 8-bit bus cycle-eater lies squarely on the 8088&#146;s external data bus. Technically, it might be more accurate to place this cycle-eater in the Bus Interface Unit, which breaks 16-bit memory accesses into paired 8-bit accesses, but it is really the limited width of the external data bus that constricts data flow into and out of the 8088. True, the original PC&#146;s bus is also only 8 bits wide, but that&#146;s just to match the 8088&#146;s 8-bit bus; even if the PC&#146;s bus were 16 bits wide, data could still pass into and out of the 8088 chip itself only 1 byte at a time.
</P>
<P>Each bus access by the 8088 takes 4 clock cycles, or 0.838 &#181;s in the 4.77 MHz PC, and transfers 1 byte. That means that the maximum rate at which data can be transferred into and out of the 8088 is 1 byte every 0.838 &#181;s. While 8086 bus accesses also take 4 clock cycles, each 8086 bus access can transfer either 1 byte or 1 word, for a maximum transfer rate of 1 word every 0.838 &#181;s. Consequently, for word-sized memory accesses, the 8086 has an effective transfer rate of 1 byte every 0.419 &#181;s. By contrast, every word-sized access on the 8088 requires two 4-cycle-long bus accesses, one for the high byte of the word and one for the low byte of the word. As a result, the 8088 has an effective transfer rate for word-sized memory accesses of just 1 word every 1.676 &#181;s&#151;and that, in a nutshell, is the 8-bit bus cycle-eater.</P>
<P>A related cycle-eater lurks beneath the 386SX chip, which is a 32-bit processor internally with only a 16-bit path to system memory. The numbers are different, but the way the cycle-eater operates is exactly the same. AT-compatible systems have 16-bit data buses, which can access a full 16-bit word at a time. The 386SX can process 32 bits (a doubleword) at a time, however, and loses a lot of time fetching that doubleword from memory in two halves.</P>
<H4 ALIGN="LEFT"><A NAME="Heading7"></A><FONT COLOR="#000077">The Impact of the 8-Bit Bus Cycle-Eater</FONT></H4>
<P>One obvious effect of the 8-bit bus cycle-eater is that word-sized accesses to memory operands on the 8088 take 4 cycles longer than byte-sized accesses. That&#146;s why the official instruction timings indicate that for code running on an 8088 an additional 4 cycles are required for every word-sized access to a memory operand. For instance,
</P>
<!-- CODE SNIP //-->
<PRE>
mov ax,word ptr [MemVar]
</PRE>
<!-- END CODE SNIP //-->
<P>takes 4 cycles longer to read the word at address <B>MemVar</B> than</P>
<!-- CODE SNIP //-->
<PRE>
mov al,byte ptr [MemVar]
</PRE>
<!-- END CODE SNIP //-->
<P>takes to read the byte at address <B>MemVar.</B> (Actually, the difference between the two isn&#146;t very likely to be exactly 4 cycles, for reasons that will become clear once we discuss the prefetch queue and dynamic RAM refresh cycle-eaters later in this chapter.)</P>
<P>What&#146;s more, in some cases one instruction can perform multiple word-sized accesses, incurring that 4-cycle penalty on each access. For example, adding a value to a word-sized memory variable requires two word-sized accesses&#151;one to read the destination operand from memory prior to adding to it, and one to write the result of the addition back to the destination operand&#151;and thus incurs not one but two 4-cycle penalties. As a result</P>
<!-- CODE SNIP //-->
<PRE>
add word ptr [MemVar],ax
</PRE>
<!-- END CODE SNIP //-->
<P>takes about 8 cycles longer to execute than:
</P>
<!-- CODE SNIP //-->
<PRE>
add byte ptr [MemVar],al
</PRE>
<!-- END CODE SNIP //-->
<P>String instructions can suffer from the 8-bit bus cycle-eater to a greater extent than other instructions. Believe it or not, a single <B>REP MOVSW</B> instruction can lose as much as 131,070 word-sized memory accesses x 4 cycles, or <I>524,280 cycles</I> to the 8-bit bus cycle-eater! In other words, one 8088 instruction (admittedly, an instruction that does a great deal) can take over one-tenth of a second longer on an 8088 than on an 8086, simply because of the 8-bit bus. <I>One-tenth of a second!</I> That&#146;s a phenomenally long time in computer terms; in one-tenth of a second, the 8088 can perform more than 50,000 additions and subtractions.</P>
<P>The upshot of all this is simply that the 8088 can transfer word-sized data to and from memory at only half the speed of the 8086, which inevitably causes performance problems when coupled with an Execution Unit that can process word-sized data every bit as quickly as an 8086. These problems show up with any code that uses word-sized memory operands. More ominously, as we will see shortly, the 8-bit bus cycle-eater can cause performance problems with other sorts of code as well.</P>
<H4 ALIGN="LEFT"><A NAME="Heading8"></A><FONT COLOR="#000077">What to Do about the 8-Bit Bus Cycle-Eater?</FONT></H4>
<P>The obvious implication of the 8-bit bus cycle-eater is that byte-sized memory variables should be used whenever possible. After all, the 8088 performs <I>byte-sized</I> memory accesses just as quickly as the 8086. For instance, Listing 4.1, which uses a byte-sized memory variable as a loop counter, runs in 10.03 s per loop. That&#146;s 20 percent faster than the 12.05 &#181;s per loop execution time of Listing 4.2, which uses a word-sized counter. Why the difference in execution times? Simply because each word-sized <B>DEC</B> performs 4 byte-sized memory accesses (two to read the word-sized operand and two to write the result back to memory), while each byte-sized <B>DEC</B> performs only 2 byte-sized memory accesses in all.</P>
<P><B>LISTING 4.1 LST4-1.ASM</B></P>
<!-- CODE //-->
<PRE>
; Measures the performance of a loop which uses a
; byte-sized memory variable as the loop counter.
;
jmp Skip
;
Counter db 100
;
Skip:
call ZTimerOn
LoopTop:
dec [Counter]
jnz LoopTop
call ZTimerOff
</PRE>
<!-- END CODE //-->
<P><B>LISTING 4.2 LST4-2.ASM</B></P>
<!-- CODE //-->
<PRE>
; Measures the performance of a loop which uses a
; word-sized memory variable as the loop counter.
;
jmp Skip
;
Counter dw 100
;
Skip:
call ZTimerOn
LoopTop:
dec [Counter]
jnz LoopTop
call ZTimerOff
</PRE>
<!-- END CODE //-->
<P>I&#146;d like to make a brief aside concerning code optimization in the listings in this book. Throughout this book I&#146;ve modeled the sample code after working code so that the timing results are applicable to real-world programming. In Listings 4.1 and 4.2, for example, I could have shown a still greater advantage for byte-sized operands simply by performing 1,000 <B>DEC</B> instructions in a row, with no branching at all. However, <B>DEC</B> instructions don&#146;t exist in a vacuum, so in the listings I used code that both decremented the counter and tested the result. The difference is that between decrementing a memory location (simply an instruction) and using a loop counter (a functional instruction sequence). If you come across code in this book that seems less than optimal, it&#146;s simply due to my desire to provide code that&#146;s relevant to real programming problems. On the other hand, optimal code is an elusive thing indeed; by no means should you assume that the code in this book is ideal! Examine it, question it, and improve upon it, for an inquisitive, skeptical mind is an important part of the Zen of assembly optimization.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

136
ch04/04-03.html Normal file
View file

@ -0,0 +1,136 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: In the Lair of the Cycle-Eaters</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=04//-->
<!--PAGES=084-087//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-04.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>Back to the 8-bit bus cycle-eater. As I&#146;ve said, in 8088 work you should strive to use byte-sized memory variables whenever possible. That does <I>not</I> mean that you should use 2 byte-sized memory accesses to manipulate a word-sized memory variable in preference to 1 word-sized memory access, as, for instance,</P>
<!-- CODE SNIP //-->
<PRE>
mov dl,byte ptr [MemVar]
mov dh,byte ptr [MemVar&#43;1]
</PRE>
<!-- END CODE SNIP //-->
<P>versus:
</P>
<!-- CODE SNIP //-->
<PRE>
mov dx,word ptr [MemVar]
</PRE>
<!-- END CODE SNIP //-->
<P>Recall that every access to a memory byte takes at least 4 cycles; that limitation is built right into the 8088. The 8088 is also built so that the second byte-sized memory access to a 16-bit memory variable takes just those 4 cycles and no more. There&#146;s no way you can manipulate the second byte of a word-sized memory variable faster with a second separate byte-sized instruction in less than 4 cycles. As a matter of fact, you&#146;re bound to access that second byte much more slowly with a separate instruction, thanks to the overhead of instruction fetching and execution, address calculation, and the like.
</P>
<P>For example, consider Listing 4.3, which performs 1,000 word-sized reads from memory. This code runs in 3.77 &#181;s per word read on a 4.77 MHz 8088. That&#146;s 45 percent faster than the 5.49 &#181;s per word read of Listing 4.4, which reads the same 1,000 words as Listing 4.3 but does so with 2,000 byte-sized reads. Both listings perform exactly the same number of memory accesses&#151;2,000 accesses, each byte-sized, as all 8088 memory accesses must be. (Remember that the Bus Interface Unit must perform two byte-sized memory accesses in order to handle a word-sized memory operand.) However, Listing 4.3 is considerably faster because it expends only 4 additional cycles to read the second byte of each word, while Listing 4.4 performs a second <B>LODSB,</B> requiring 13 cycles, to read the second byte of each word.</P>
<P><B>LISTING 4.3 LST4-3.ASM</B></P>
<!-- CODE SNIP //-->
<PRE>
; Measures the performance of reading 1,000 words
; from memory with 1,000 word-sized accesses.
;
sub si,si
mov cx,1000
call ZTimerOn
rep lodsw
call ZTimerOff
</PRE>
<!-- END CODE SNIP //-->
<P><B>LISTING 4.4 LST4-4.ASM</B></P>
<!-- CODE SNIP //-->
<PRE>
; Measures the performance of reading 1000 words
; from memory with 2,000 byte-sized accesses.
;
sub si,si
mov cx,2000
call ZTimerOn
rep lodsb
call ZTimerOff
</PRE>
<!-- END CODE SNIP //-->
<P>In short, if you must perform a 16-bit memory access, let the 8088 break the access into two byte-sized accesses for you. The 8088 is more efficient at that task than your code can possibly be.
</P>
<P>Word-sized variables should be stored in registers to the greatest feasible extent, since registers are inside the 8088, where 16-bit operations are just as fast as 8-bit operations because the 8-bit cycle-eater can&#146;t get at them. In fact, it&#146;s a good idea to keep as many variables of all sorts in registers as you can. Instructions with register-only operands execute very rapidly, partially because they avoid both the time-consuming memory accesses and the lengthy address calculations associated with memory operands.</P>
<P>There is yet another reason why register operands are preferable to memory operands, and it&#146;s an unexpected effect of the 8-bit bus cycle-eater. Instructions with only register operands tend to be shorter (in terms of bytes) than instructions with memory operands, and when it comes to performance, shorter is usually better. In order to explain why that is true and how it relates to the 8-bit bus cycle-eater, I must diverge for a moment.</P>
<P>For the last few pages, you may well have been thinking that the 8-bit bus cycle-eater, while a nuisance, doesn&#146;t seem particularly subtle or difficult to quantify. After all, any instruction reference tells us exactly how many cycles each instruction loses to the 8-bit bus cycle-eater, doesn&#146;t it?</P>
<P>Yes and no. It&#146;s true that in general we know approximately how much longer a given instruction will take to execute with a word-sized memory operand than with a byte-sized operand, although the dynamic RAM refresh and wait state cycle-eaters (which I&#146;ll cover a little later) can raise the cost of the 8-bit bus cycle-eater considerably. However, <I>all</I> word-sized memory accesses lose 4 cycles to the 8-bit bus cycle-eater, and there&#146;s one sort of word-sized memory access we haven&#146;t discussed yet: instruction fetching. The ugliest manifestation of the 8-bit bus cycle-eater is in fact the prefetch queue cycle-eater.</P>
<H3><A NAME="Heading9"></A><FONT COLOR="#000077">The Prefetch Queue Cycle-Eater</FONT></H3>
<P>In an 8088 context, here&#146;s the prefetch queue cycle-eater in a nutshell: The 8088&#146;s 8-bit external data bus keeps the Bus Interface Unit from fetching instruction bytes as fast as the 16-bit Execution Unit can execute them, so the Execution Unit often lies idle while waiting for the next instruction byte to be fetched.
</P>
<P>Exactly why does this happen? Recall that the 8088 is an 8086 internally, but accesses word-sized memory data at only one-half the maximum rate of the 8086 due to the 8088&#146;s 8-bit external data bus. Unfortunately, instructions are among the word-sized data the 8086 fetches, meaning that the 8088 can fetch instructions at only one-half the speed of the 8086. On the other hand, the 8086-equivalent Execution Unit of the 8088 can <I>execute</I> instructions every bit as fast as the 8086. The net result is that the Execution Unit burns up instruction bytes much faster than the Bus Interface Unit can fetch them, and ends up idling while waiting for instructions bytes to arrive.</P>
<P>The BIU can fetch instruction bytes at a maximum rate of one byte every 4 cycles&#151;<I>and that 4-cycle per instruction byte rate is the ultimate limit on overall instruction execution time, regardless of EU speed.</I> While the EU may execute a given instruction that&#146;s already in the prefetch queue in less than 4 cycles per byte, over time the EU can&#146;t execute instructions any faster than they can arrive&#151;and they can&#146;t arrive faster than 1 byte every 4 cycles.</P>
<P>Clearly, then, the prefetch queue cycle-eater is nothing more than one aspect of the 8-bit bus cycle-eater. 8088 code often runs at less than the Execution Unit&#146;s maximum speed because the 8-bit data bus can&#146;t keep up with the demand for instruction bytes. That&#146;s straightforward enough&#151;so why all the fuss about the prefetch queue cycle-eater?</P>
<P>What makes the prefetch queue cycle-eater tricky is that it&#146;s undocumented and unpredictable. That is, with a word-sized memory access, such as</P>
<!-- CODE SNIP //-->
<PRE>
mov [bx],ax
</PRE>
<!-- END CODE SNIP //-->
<P>it&#146;s well-documented that an extra 4 cycles will always be required to write the upper byte of AX to memory. Not so with the prefetch queue cycle-eater lurking nearby. For instance, the instructions
</P>
<!-- CODE SNIP //-->
<PRE>
shr ax,1
shr ax,1
shr ax,1
shr ax,1
shr ax,1
</PRE>
<!-- END CODE SNIP //-->
<P>should execute in 10 cycles, since each <B>SHR</B> takes 2 cycles to execute, according to Intel&#146;s specifications. Those specifications contain Intel&#146;s official instruction execution times, but in this case&#151;and in many others&#151;the specifications are drastically wrong. Why? Because they describe execution time <I>once an instruction reaches the prefetch queue.</I> They say nothing about whether a given instruction will be in the prefetch queue when it&#146;s time for that instruction to run, or how long it will take that instruction to reach the prefetch queue if it&#146;s not there already. Thanks to the low performance of the 8088&#146;s external data bus, that&#146;s a glaring omission&#151;but, alas, an unavoidable one. Let&#146;s look at why the official execution times are wrong, and why that can&#146;t be helped.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-04.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

115
ch04/04-04.html Normal file
View file

@ -0,0 +1,115 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: In the Lair of the Cycle-Eaters</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=04//-->
<!--PAGES=087-090//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-05.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H4 ALIGN="LEFT"><A NAME="Heading10"></A><FONT COLOR="#000077">Official Execution Times Are Only Part of the Story</FONT></H4>
<P>The sequence of 5 <B>SHR</B> instructions in the last example is 10 bytes long. That means that it can never execute in less than 24 cycles even if the 4-byte prefetch queue is full when it starts, since 6 instruction bytes would still remain to be fetched, at 4 cycles per fetch. If the prefetch queue is empty at the start, the sequence <I>could</I> take 40 cycles. In short, thanks to instruction fetching, the code won&#146;t run at its documented speed, and could take up to four times longer than it is supposed to.</P>
<P>Why does Intel document Execution Unit execution time rather than overall instruction execution time, which includes both instruction fetch time and Execution Unit (EU) execution time? Well, instruction fetching isn&#146;t performed as part of instruction execution by the Execution Unit, but instead is carried on in parallel by the Bus Interface Unit (BIU) whenever the external data bus isn&#146;t in use or whenever the EU runs out of instruction bytes to execute. Sometimes the BIU is able to use spare bus cycles to prefetch instruction bytes before the EU needs them, so in those cases instruction fetching takes no time at all, practically speaking. At other times the EU executes instructions faster than the BIU can fetch them, and instruction fetching then becomes a significant part of overall execution time. As a result, <I>the effective fetch time for a given instruction varies greatly depending on the code mix preceding that instruction.</I> Similarly, the state in which a given instruction leaves the prefetch queue affects the overall execution time of the following instructions.</P>
<TABLE WIDTH="100%"><TD VALIGN="TOP" ALIGN="LEFT" WIDTH="5%"><IMG SRC="images/04-01i.jpg"><TD VALIGN="TOP" ALIGN="LEFT" WIDTH="95%"><SMALL><I>In other words, while the execution time for a given instruction is constant, the fetch time for that instruction depends heavily on the context in which the instruction is executing&#151;the amount of prefetching the preceding instructions allowed&#151;and can vary from a full 4 cycles per instruction byte to no time at all.</I></SMALL>
</TABLE>
<P>As we&#146;ll see later, other cycle-eaters, such as DRAM refresh and display memory wait states, can cause prefetching variations even during different executions of the same code sequence. Given that, it&#146;s meaningless to talk about the prefetch time of a given instruction except in the context of a specific code sequence.
</P>
<P>So now you know why the official instruction execution times are often wrong, and why Intel can&#146;t provide better specifications. You also know now why it is that you must time your code if you want to know how fast it really is.</P>
<H4 ALIGN="LEFT"><A NAME="Heading11"></A><FONT COLOR="#000077">There Is No Such Beast as a True Instruction Execution Time</FONT></H4>
<P>The effect of the code preceding an instruction on the execution time of that instruction makes the Zen timer trickier to use than you might expect, and complicates the interpretation of the results reported by the Zen timer. For one thing, the Zen timer is best used to time code sequences that are more than a few instructions long; below 10&#181;s or so, prefetch queue effects and the limited resolution of the clock driving the timer can cause problems.
</P>
<P>Some slight prefetch queue-induced inaccuracy usually exists even when the Zen timer is used to time longer code sequences, since the calls to the Zen timer usually alter the code&#146;s prefetch queue from its normal state. (Branches&#151;jumps, calls, returns and the like&#151;empty the prefetch queue.) Ideally, the Zen timer is used to measure the performance of an entire subroutine, so the prefetch queue effects of the branches at the start and end of the subroutine are similar to the effects of the calls to the Zen timer when you&#146;re measuring the subroutine&#146;s performance.</P>
<P>Another way in which the prefetch queue cycle-eater complicates the use of the Zen timer involves the practice of timing the performance of a few instructions over and over. I&#146;ll often repeat one or two instructions 100 or 1,000 times in a row in listings in this book in order to get timing intervals that are long enough to provide reliable measurements. However, as we just learned, the actual performance of any 8088 instruction depends on the code mix preceding any given use of that instruction, which in turn affects the state of the prefetch queue when the instruction starts executing. Alas, the execution time of an instruction preceded by dozens of identical instructions reflects just one of many possible prefetch states (and not a very likely state at that), and some of the other prefetch states may well produce distinctly different results.</P>
<P>For example, consider the code in Listings 4.5 and 4.6. Listing 4.5 shows our familiar <B>SHR</B> case. Here, because the prefetch queue is always empty, execution time should work out to about 4 cycles per byte, or 8 cycles per <B>SHR,</B> as shown in Figure 4.3. (Figure 4.3 illustrates the relationship between instruction fetching and execution in a simplified way, and is not intended to show the exact timings of 8088 operations.) That&#146;s quite a contrast to the official 2-cycle execution time of <B>SHR</B>. In fact, the Zen timer reports that Listing 4.5 executes in 1.81&#181;s per byte, or slightly <I>more</I> than 4 cycles per byte. (The extra time is the result of the dynamic RAM refresh cycle-eater, which we&#146;ll discuss shortly.) Going by Listing 4.5, we would conclude that the &#147;true&#148; execution time of <B>SHR</B> is 8.64 cycles.</P>
<P><B>LISTING 4.5 LST4-5.ASM</B></P>
<!-- CODE //-->
<PRE>
; Measures the performance of 1,000 SHR instructions
; in a row. Since SHR executes in 2 cycles but is
; 2 bytes long, the prefetch queue is always empty,
; and prefetching time determines the overall
; performance of the code.
;
call ZTimerOn
rept 1000
shr ax,1
endm
call ZTimerOff
</PRE>
<!-- END CODE //-->
<P><B>LISTING 4.6 LST4-6.ASM</B></P>
<!-- CODE //-->
<PRE>
; Measures the performance of 1,000 MUL/SHR instruction
; pairs in a row. The lengthy execution time of MUL
; should keep the prefetch queue from ever emptying.
;
mov cx,1000
sub ax,ax
call ZTimerOn
rept 1000
mul ax
shr ax,1
endm
call ZTimerOff
</PRE>
<!-- END CODE //-->
<P><A NAME="Fig3"><!-- </A><A HREF="javascript:displayWindow('images/04-03.jpg',414,337 )"> --><IMG SRC="images/04-03.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/04-03.jpg',414,337)"> --><FONT COLOR="#000077"><B>Figure 4.3</B></FONT></A>&nbsp;&nbsp;<I>Execution and instruction prefetching sequence for Listing 4.5.</I>
</P>
<P>Now let&#146;s examine Listing 4.6. Here each <B>SHR</B> follows a <B>MUL</B> instruction. Since <B>MUL</B> instructions take so long to execute that the prefetch queue is always full when they finish, each <B>SHR</B> should be ready and waiting in the prefetch queue when the preceding <B>MUL</B> ends. As a result, we&#146;d expect that each <B>SHR</B> would execute in 2 cycles; together with the 118-cycle execution time of multiplying 0 times 0, the total execution time should come to 120 cycles per <B>SHR/MUL</B> pair, as shown in Figure 4.4. And, by God, when we run Listing 4.6 we get an execution time of 25.14 &#181;s per <B>SHR/MUL</B> pair, or <I>exactly</I> 120 cycles! According to these results, the &#147;true&#148; execution time of <B>SHR</B> would seem to be 2 cycles, quite a change from the conclusion we drew from Listing 4.5.</P>
<P>The key point is this: We&#146;ve seen one code sequence in which <B>SHR</B> took 8-plus cycles to execute, and another in which it took only 2 cycles. Are we talking about two different forms of <B>SHR</B> here? Of course not&#151;the difference is purely a reflection of the differing states in which the preceding code left the prefetch queue. In Listing 4.5, each <B>SHR</B> after the first few follows a slew of other <B>SHR</B> instructions which have sucked the prefetch queue dry, so overall performance reflects instruction fetch time. By contrast, each <B>SHR</B> in Listing 4.6 follows a <B>MUL</B> instruction which leaves the prefetch queue full, so overall performance reflects Execution Unit execution time.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-05.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

111
ch04/04-05.html Normal file
View file

@ -0,0 +1,111 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: In the Lair of the Cycle-Eaters</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=04//-->
<!--PAGES=090-093//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-04.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-06.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>Clearly, either instruction fetch time <I>or</I> Execution Unit execution time&#151;or even a mix of the two, if an instruction is partially prefetched&#151;can determine code performance. Some people operate under a rule of thumb by which they assume that the execution time of each instruction is 4 cycles times the number of bytes in the instruction. While that&#146;s often true for register-only code, it frequently doesn&#146;t hold for code that accesses memory. For one thing, the rule should be 4 cycles times the number of <I>memory accesses,</I> not instruction bytes, since all accesses take 4 cycles on the 8088-based PC. For another, memory-accessing instructions often have slower Execution Unit execution times than the 4 cycles per memory access rule would dictate, because the 8088 isn&#146;t very fast at calculating memory addresses. Also, the 4 cycles per instruction byte rule isn&#146;t true for register-only instructions that are already in the prefetch queue when the preceding instruction ends.</P>
<P>The truth is that it never hurts performance to reduce either the cycle count or the byte count of a given bit of code, but there&#146;s no guarantee that one or the other will improve performance either. For example, consider Listing 4.7, which consists of a series of 4-cycle, 2-byte <B>MOV AL,0</B> instructions, and which executes at the rate of 1.81 &#181;s per instruction. Now consider Listing 4.8, which replaces the 4-cycle <B>MOV AL,0</B> with the 3-cycle (but still 2-byte) <B>SUB AL,AL,</B> Despite its 1-cycle-per-instruction advantage, Listing 4.8 runs at exactly the same speed as Listing 4.7. The reason: Both instructions are 2 bytes long, and in both cases it is the 8-cycle instruction fetch time, not the 3 or 4-cycle Execution Unit execution time, that limits performance.</P>
<P><A NAME="Fig4"><!-- </A><A HREF="javascript:displayWindow('images/04-04.jpg',410,449 )"> --><IMG SRC="images/04-04.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/04-04.jpg',410,449)"> --><FONT COLOR="#000077"><B>Figure 4.4</B></FONT></A>&nbsp;&nbsp;<I>Execution and instruction prefetching sequence for Listing 4.6.</I>
</P>
<P><B>LISTING 4.7 LST4-7.ASM</B></P>
<!-- CODE //-->
<PRE>
; Measures the performance of repeated MOV AL,0 instructions,
; which take 4 cycles each according to Intel's official
; specifications.
;
sub ax,ax
call ZTimerOn
rept 1000
mov al,0
endm
call ZTimerOff
</PRE>
<!-- END CODE //-->
<P><B>LISTING 4.8 LST4-8.ASM</B></P>
<!-- CODE //-->
<PRE>
; Measures the performance of repeated SUB AL,AL instructions,
; which take 3 cycles each according to Intel's official
; specifications.
;
sub ax,ax
call ZTimerOn
rept 1000
sub al,al
endm
call ZTimerOff
</PRE>
<!-- END CODE //-->
<P>As you can see, it&#146;s easy to be drawn into thinking you&#146;re saving cycles when you&#146;re not. You can only improve the performance of a specific bit of code by reducing the factor&#151;either instruction fetch time or execution time, or sometimes a mix of the two&#151;that&#146;s limiting the performance of that code.
</P>
<P>In case you missed it in all the excitement, the variability of prefetching means that our method of testing performance by executing 1,000 instructions in a row by no means produces &#147;true&#148; instruction execution times, any more than the official execution times in the Intel manuals are &#147;true&#148; times. The fact of the matter is that a given instruction takes <I>at least</I> as long to execute as the time given for it in the Intel manuals, but may take as much as 4 cycles per byte longer, depending on the state of the prefetch queue when the preceding instruction ends.</P>
<TABLE WIDTH="100%"><TD VALIGN="TOP" ALIGN="LEFT" WIDTH="5%"><IMG SRC="images/04-02i.jpg"><TD VALIGN="TOP" ALIGN="LEFT" WIDTH="95%"><SMALL><I>The only true execution time for an instruction is a time measured in a certain context, and that time is meaningful only in that context.</I></SMALL>
</TABLE>
<P>What we <I>really</I> want is to know how long useful working code takes to run, not how long a single instruction takes, and the Zen timer gives us the tool we need to gather that information. Granted, it would be easier if we could just add up neatly documented instruction execution times&#151;but that&#146;s not going to happen. Without actually measuring the performance of a given code sequence, you simply don&#146;t know how fast it is. For crying out loud, even the people who <I>designed</I> the 8088 at Intel couldn&#146;t tell you exactly how quickly a given 8088 code sequence executes on the PC just by looking at it! Get used to the idea that execution times are only meaningful in context, learn the rules of thumb in this book, and use the Zen timer to measure your code.</P>
<H4 ALIGN="LEFT"><A NAME="Heading12"></A><FONT COLOR="#000077">Approximating Overall Execution Times</FONT></H4>
<P>Don&#146;t think that because overall instruction execution time is determined by both instruction fetch time and Execution Unit execution time, the two times should be added together when estimating performance. For example, practically speaking, each <B>SHR</B> in Listing 4.5 does not take 8 cycles of instruction fetch time plus 2 cycles of Execution Unit execution time to execute. Figure 4.3 shows that while a given <B>SHR</B> is executing, the fetch of the next <B>SHR</B> is starting, and since the two operations are overlapped for 2 cycles, there&#146;s no sense in charging the time to both instructions. You could think of the extra instruction fetch time for <B>SHR</B> in Listing 4.5 as being 6 cycles, which yields an overall execution time of 8 cycles when added to the 2 cycles of Execution Unit execution time.</P>
<P>Alternatively, you could think of each <B>SHR</B> in Listing 4.5 as taking 8 cycles to fetch, and then executing in effectively 0 cycles while the next <B>SHR</B> is being fetched. Whichever perspective you prefer is fine. The important point is that the time during which the execution of one instruction and the fetching of the next instruction overlap should only be counted toward the overall execution time of one of the instructions. For all intents and purposes, one of the two instructions runs at no performance cost whatsoever while the overlap exists.</P>
<P>As a working definition, we&#146;ll consider the execution time of a given instruction in a particular context to start when the first byte of the instruction is sent to the Execution Unit and end when the first byte of the next instruction is sent to the EU.</P>
<H4 ALIGN="LEFT"><A NAME="Heading13"></A><FONT COLOR="#000077">What to Do about the Prefetch Queue Cycle-Eater?</FONT></H4>
<P>Reducing the impact of the prefetch queue cycle-eater is one of the overriding principles of high-performance assembly code. How can you do this? One effective technique is to minimize access to memory operands, since such accesses compete with instruction fetching for precious memory accesses. You can also greatly reduce instruction fetch time simply by your choice of instructions: <I>Keep your instructions short.</I> Less time is required to fetch instructions that are 1 or 2 bytes long than instructions that are 5 or 6 bytes long. Reduced instruction fetching lowers minimum execution time (minimum execution time is 4 cycles times the number of instruction bytes) and often leads to faster overall execution.</P>
<P>While short instructions minimize overall prefetch time, ironically they actually often suffer more from the prefetch queue bottleneck than do long instructions. Short instructions generally have such fast execution times that they drain the prefetch queue despite their small size. For example, consider the <B>SHR</B> of Listing 4.5, which runs at only 25 percent of its Execution Unit execution time even though it&#146;s only 2 bytes long, thanks to the prefetch queue bottleneck. Short instructions are nonetheless generally faster than long instructions, thanks to the combination of fewer instruction bytes and faster Execution Unit execution times, and should be used as much as possible&#151;just don&#146;t expect them to run at their &#147;official&#148; documented speeds.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-04.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-06.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

83
ch04/04-06.html Normal file
View file

@ -0,0 +1,83 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: In the Lair of the Cycle-Eaters</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=04//-->
<!--PAGES=094-097//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-05.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-07.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>More than anything, the above rules mean using the registers as heavily as possible, both because register-only instructions are short and because they don&#146;t perform memory accesses to read or write operands. However, using the registers is a rule of thumb, not a commandment. In some circumstances, it may actually be <I>faster</I> to access memory. (The look-up table technique is one such case.) What&#146;s more, the performance of the prefetch queue (and hence the performance of each instruction) differs from one code sequence to the next, and can even differ during different executions of the <I>same</I> code sequence.</P>
<P>All in all, writing good assembler code is as much an art as a science. As a result, you should follow the rules of thumb described here&#151;and then time your code to see how fast it really is. You should experiment freely, but always remember that actual, measured performance is the bottom line.</P>
<H4 ALIGN="LEFT"><A NAME="Heading14"></A><FONT COLOR="#000077">Holding Up the 8088</FONT></H4>
<P>In this chapter I&#146;ve taken you further and further into the depths of the PC, telling you again and again that you must understand the computer at the lowest possible level in order to write good code. At this point, you may well wonder, &#147;Have we gotten low enough?&#148;
</P>
<P>Not quite yet. The 8-bit bus and prefetch queue cycle-eaters are low-level indeed, but we&#146;ve one level yet to go. Dynamic RAM refresh and wait states&#151;our next topics&#151;together form the lowest level at which the hardware of the PC affects code performance. Below this level, the PC is of interest only to hardware engineers.</P>
<P>Before we begin our discussion of dynamic RAM refresh, let&#146;s step back for a moment to take an overall look at this lowest level of cycle-eaters. In truth, the distinctions between wait states and dynamic RAM refresh don&#146;t much matter to a programmer. What is important is that you understand this: <I>Under certain circumstances, devices on the PC bus can stop the CPU for 1 or more cycles, making your code run more slowly than it seemingly should.</I></P>
<P>Unlike all the cycle-eaters we&#146;ve encountered so far, wait states and dynamic RAM refresh are strictly external to the CPU, as was shown in Figure 4.1. Adapters on the PC&#146;s bus, such as video and memory cards, can insert wait states on any bus access, the idea being that they won&#146;t be able to complete the access properly unless the access is stretched out. Likewise, the channel of the DMA controller dedicated to dynamic RAM refresh can request control of the bus at any time, although the CPU must relinquish the bus before the DMA controller can take over. This means that your code can&#146;t directly control wait states or dynamic RAM refresh. However, code <I>can</I> sometimes be designed to minimize the effects of these cycle-eaters, and even when the cycle-eaters slow your code without there being a thing in the world you can do about it, you&#146;re still better off understanding that you&#146;re losing performance and knowing why your code doesn&#146;t run as fast as it&#146;s supposed to than you were programming in ignorance.</P>
<P>Let&#146;s start with DRAM refresh, which affects the performance of every program that runs on the PC.</P>
<H3><A NAME="Heading15"></A><FONT COLOR="#000077">Dynamic RAM Refresh: The Invisible Hand</FONT></H3>
<P>Dynamic RAM (DRAM) refresh is sort of an act of God. By that I mean that DRAM refresh invisibly and inexorably steals a certain fraction of all available memory access time from your programs, when they are accessing memory for code and data. (When they are accessing cache on more recent processors, theoretically the DRAM refresh cycle-eater doesn&#146;t come into play, but there are other cycle-eaters waiting to prey on cache-bound programs.) While you <I>could</I> stop DRAM refresh, you wouldn&#146;t want to since that would be a sure prescription for crashing your computer. In the end, thanks to DRAM refresh, almost all code runs a bit slower on the PC than it otherwise would, and that&#146;s that.</P>
<P>A bit of background: A static RAM (SRAM) chip is a memory chip that retains its contents indefinitely so long as power is maintained. By contrast, each of several blocks of bits in a dynamic RAM (DRAM) chip retains its contents for only a short time after it&#146;s accessed for a read or write. In order to get a DRAM chip to store data for an extended period, each of the blocks of bits in that chip must be accessed regularly, so that the chip&#146;s stored data is kept refreshed and valid. So long as this is done often enough, a DRAM chip will retain its contents indefinitely.</P>
<P>All of the PC&#146;s system memory consists of DRAM chips. Each DRAM chip in the PC must be completely refreshed about once every four milliseconds in order to ensure the integrity of the data it stores. Obviously, it&#146;s highly desirable that the memory in the PC retain the correct data indefinitely, so each DRAM chip in the PC <I>must</I> always be refreshed within 4 &#181;s of the last refresh. Since there&#146;s no guarantee that a given program will access each and every DRAM block once every 4 &#181;s, the PC contains special circuitry and programming for providing DRAM refresh.</P>
<H4 ALIGN="LEFT"><A NAME="Heading16"></A><FONT COLOR="#000077">How DRAM Refresh Works in the PC</FONT></H4>
<P>On the original 8088-based IBM PC, timer 1 of the 8253 timer chip is programmed at power-up to generate a signal once every 72 cycles, or once every 15.08&#181;s. That signal goes to channel 0 of the 8237 DMA controller, which requests the bus from the 8088 upon receiving the signal. (DMA stands for <I>direct memory access,</I> the ability of a device other than the 8088 to control the bus and access memory directly, without any help from the 8088.) As soon as the 8088 is between memory accesses, it gives control of the bus to the 8237, which in conjunction with special circuitry on the PC&#146;s motherboard then performs a single 4-cycle read access to 1 of 256 possible addresses, advancing to the next address on each successive access. (The read access is only for the purpose of refreshing the DRAM; the data that is read isn&#146;t used.)</P>
<P>The 256 addresses accessed by the refresh DMA accesses are arranged so that taken together they properly refresh all the memory in the PC. By accessing one of the 256 addresses every 15.08 &#181;s, all of the PC&#146;s DRAM is refreshed in 256 x 15.08 &#181;s, or 3.86 &#181;s, which is just about the desired 4 &#181;s time I mentioned earlier. (Only the first 640K of memory is refreshed in the PC; video adapters and other adapters above 640K containing memory that requires refreshing must provide their own DRAM refresh in pre-AT systems.)</P>
<P>Don&#146;t sweat the details here. The important point is this: For at least 4 out of every 72 cycles, the original PC&#146;s bus is given over to DRAM refresh and is not available to the 8088, as shown in Figure 4.5. That means that as much as 5.56 percent of the PC&#146;s already inadequate bus capacity is lost. However, DRAM refresh doesn&#146;t necessarily stop the 8088 in its tracks for 4 cycles. The Execution Unit of the 8088 can keep processing while DRAM refresh is occurring, unless the EU needs to access memory. Consequently, DRAM refresh can slow code performance anywhere from 0 percent to 5.56 percent (and actually a bit more, as we'll see shortly), depending on the extent to which DRAM refresh occupies cycles during which the 8088 would otherwise be accessing memory.</P>
<P><A NAME="Fig5"><!-- </A><A HREF="javascript:displayWindow('images/04-05.jpg',407,339 )"> --><IMG SRC="images/04-05.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/04-05.jpg',407,339)"> --><FONT COLOR="#000077"><B>Figure 4.5</B></FONT></A>&nbsp;&nbsp;<I>The PC bus dynamic RAM (DRAM) refresh.</I>
<P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-05.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-07.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

110
ch04/04-07.html Normal file
View file

@ -0,0 +1,110 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: In the Lair of the Cycle-Eaters</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=04//-->
<!--PAGES=097-099//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-06.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-08.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
</P>
<H4 ALIGN="LEFT"><A NAME="Heading17"></A><FONT COLOR="#000077">The Impact of DRAM Refresh</FONT></H4>
<P>Let&#146;s look at examples from opposite ends of the spectrum in terms of the impact of DRAM refresh on code performance. First, consider the series of <B>MUL</B> instructions in Listing 4.9. Since a 16-bit <B>MUL</B> on the 8088 executes in between 118 and 133 cycles and is only 2 bytes long, there should be plenty of time for the prefetch queue to fill after each instruction, even after DRAM refresh has taken its slice of memory access time. Consequently, the prefetch queue should be able to keep the Execution Unit well-supplied with instruction bytes at all times. Since Listing 4.9 uses no memory operands, the Execution Unit should never have to wait for data from memory, and DRAM refresh should have no impact on performance. (Remember that the Execution Unit can operate normally during DRAM refreshes so long as it doesn&#146;t need to request a memory access from the Bus Interface Unit.)</P>
<P><B>LISTING 4.9 LST4-9.ASM</B></P>
<!-- CODE //-->
<PRE>
; Measures the performance of repeated MUL instructions,
; which allow the prefetch queue to be full at all times,
; to demonstrate a case in which DRAM refresh has no impact
; on code performance.
;
sub ax,ax
call ZTimerOn
rept 1000
mul ax
endm
call ZTimerOff
</PRE>
<!-- END CODE //-->
<P>Running Listing 4.9, we find that each <B>MUL</B> executes in 24.72 &#181;s, or exactly 118 cycles. Since that&#146;s the shortest time in which <B>MUL</B> can execute, we can see that no performance is lost to DRAM refresh. Listing 4.9 clearly illustrates that DRAM refresh only affects code performance when a DRAM refresh forces the Execution Unit of the 8088 to wait for a memory access.</P>
<P>Now let&#146;s look at the series of <B>SHR</B> instructions shown in Listing 4.10. Since <B>SHR</B> executes in 2 cycles but is 2 bytes long, the prefetch queue should be empty while Listing 4.10 executes, with the 8088 prefetching instruction bytes non-stop. As a result, the time per instruction of Listing 4.10 should precisely reflect the time required to fetch the instruction bytes.</P>
<P><B>LISTING 4.10 LST4-10.ASM</B></P>
<!-- CODE //-->
<PRE>
; Measures the performance of repeated SHR instructions,
; which empty the prefetch queue, to demonstrate the
; worst-case impact of DRAM refresh on code performance.
;
call ZTimerOn
rept 1000
shr ax,1
endm
call ZTimerOff
</PRE>
<!-- END CODE //-->
<P>Since 4 cycles are required to read each instruction byte, we&#146;d expect each <B>SHR</B> to execute in 8 cycles, or 1.676 &#181;s, if there were no DRAM refresh. In fact, each <B>SHR</B> in Listing 4.10 executes in 1.81 &#181;s, indicating that DRAM refresh is taking 7.4 percent of the program&#146;s execution time. That&#146;s nearly 2 percent more than our worst-case estimate of the loss to DRAM refresh overhead! In fact, the result indicates that DRAM refresh is stealing not 4, but 5.33 cycles out of every 72 cycles. How can this be?</P>
<P>The answer is that a given DRAM refresh can actually hold up CPU memory accesses for as many as 6 cycles, depending on the timing of the DRAM refresh&#146;s DMA request relative to the 8088&#146;s internal instruction execution state. When the code in Listing 4.10 runs, each DRAM refresh holds up the CPU for either 5 or 6 cycles, depending on where the 8088 is in executing the current <B>SHR</B> instruction when the refresh request occurs. Now we see that things can get even worse than we thought: <I>DRAM refresh can steal as much as 8.33 percent of available memory access time&#151;6 out of every 72 cycles&#151;from the 8088.</I></P>
<P>Which of the two cases we&#146;ve examined reflects reality? While either case <I>can</I> happen, the latter case&#151;significant performance reduction, ranging as high as 8.33 percent&#151;is far more likely to occur. This is especially true for high-performance assembly code, which uses fast instructions that tend to cause non-stop instruction fetching.</P>
<H4 ALIGN="LEFT"><A NAME="Heading18"></A><FONT COLOR="#000077">What to Do About the DRAM Refresh Cycle-Eater?</FONT></H4>
<P><I>Hmmm.</I> When we discovered the prefetch queue cycle-eater, we learned to use short instructions. When we discovered the 8-bit bus cycle-eater, we learned to use byte-sized memory operands whenever possible, and to keep word-sized variables in registers. What can we do to work around the DRAM refresh cycle-eater?</P>
<P>Nothing.</P>
<P>As I&#146;ve said before, DRAM refresh is an act of God. DRAM refresh is a fundamental, unchanging part of the PC&#146;s operation, and there&#146;s nothing you or I can do about it. If refresh were any less frequent, the reliability of the PC would be compromised, so tinkering with either timer 1 or DMA channel 0 to reduce DRAM refresh overhead is out. Nor is there any way to structure code to minimize the impact of DRAM refresh. Sure, some instructions are affected less by DRAM refresh than others, but how many multiplies and divides in a row can you really use? I suppose that code <I>could</I> conceivably be structured to leave a free memory access every 72 cycles, so DRAM refresh wouldn&#146;t have any effect. In the old days when code size was measured in bytes, not K bytes, and processors were less powerful&#151;and complex&#151;programmers did in fact use similar tricks to eke every last bit of performance from their code. When programming the PC, however, the prefetch queue cycle-eater would make such careful code synchronization a difficult task indeed, and any modest performance improvement that did result could never justify the increase in programming complexity and the limits on creative programming that such an approach would entail. Besides, all that effort goes to waste on faster 8088s, 286s, and other computers with different execution speeds and refresh characteristics. There&#146;s no way around it: Useful code accesses memory frequently and at irregular intervals, and over the long haul DRAM refresh always exacts its price.</P>
<P>If you&#146;re still harboring thoughts of reducing the overhead of DRAM refresh, consider this. Instructions that tend not to suffer very much from DRAM refresh are those that have a high ratio of execution time to instruction fetch time, and those aren&#146;t the fastest instructions of the PC. It certainly wouldn&#146;t make sense to use slower instructions just to reduce DRAM refresh overhead, for it&#146;s <I>total</I> execution time&#151;DRAM refresh, instruction fetching, and all&#151;that matters.</P>
<P>The important thing to understand about DRAM refresh is that it generally slows your code down, and that the extent of that performance reduction can vary considerably and unpredictably, depending on how the DRAM refreshes interact with your code&#146;s pattern of memory accesses. When you use the Zen timer and get a fractional cycle count for the execution time of an instruction, that&#146;s often the DRAM refresh cycle-eater at work. (The display adapter cycleis another possible culprit, and, on 386s and later processors, cache misses and pipeline execution hazards produce this sort of effect as well.) Whenever you get two timing results that differ less or more than they seemingly should, that&#146;s usually DRAM refresh too. Thanks to DRAM refresh, variations of up to 8.33 percent in PC code performance are par for the course.</P>
<H3><A NAME="Heading19"></A><FONT COLOR="#000077">Wait States</FONT></H3>
<P>Wait states are cycles during which a bus access by the CPU to a device on the PC&#146;s bus is temporarily halted by that device while the device gets ready to complete the read or write. Wait states are well and truly the lowest level of code performance. Everything we have discussed (and will discuss)&#151;even DMA accesses&#151;can be affected by wait states.
</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-06.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-08.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

89
ch04/04-08.html Normal file
View file

@ -0,0 +1,89 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: In the Lair of the Cycle-Eaters</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=04//-->
<!--PAGES=099-104//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-07.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-09.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>Wait states exist because the CPU must to be able to coexist with any adapter, no matter how slow (within reason). The 8088 expects to be able to complete each bus access&#151;a memory or I/O read or write&#151;in 4 cycles, but adapters can&#146;t always respond that quickly for a number of reasons. For example, display adapters must split access to display memory between the CPU and the circuitry that generates the video signal based on the contents of display memory, so they often can&#146;t immediately fulfill a request by the CPU for a display memory read or write. To resolve this conflict, display adapters can tell the CPU to wait during bus accesses by inserting one or more wait states, as shown in Figure 4.6. The CPU simply sits and idles as long as wait states are inserted, then completes the access as soon as the display adapter indicates its readiness by no longer inserting wait states. The same would be true of any adapter that couldn&#146;t keep up with the CPU.
</P>
<P>Mind you, this is all transparent to executing code. An instruction that encounters wait states runs exactly as if there were no wait states, only slower. Wait states are nothing more or less than wasted time as far as the CPU and your program are concerned.</P>
<P>By understanding the circumstances in which wait states can occur, you can avoid them when possible. Even when it&#146;s not possible to work around wait states, it&#146;s still to your advantage to understand how they can cause your code to run more slowly.</P>
<P>First, let&#146;s learn a bit more about wait states by contrast with DRAM refresh. Unlike DRAM refresh, wait states do not occur on any regularly scheduled basis, and are of no particular duration. Wait states can only occur when an instruction performs a memory or I/O read or write. Both the presence of wait states and the number of wait states inserted on any given bus access are entirely controlled by the device being accessed. When it comes to wait states, the CPU is passive, merely accepting whatever wait states the accessed device chooses to insert during the course of the access. All of this makes perfect sense given that the whole point of the wait state mechanism is to allow a device to stretch out any access to itself for however much time it needs to perform the access.</P>
<P><A NAME="Fig6"><!-- </A><A HREF="javascript:displayWindow('images/04-06.jpg',415,242 )"> --><IMG SRC="images/04-06.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/04-06.jpg',415,242)"> --><FONT COLOR="#000077"><B>Figure 4.6</B></FONT></A>&nbsp;&nbsp;<I>Video wait states inserted by the display adapter.</I>
</P>
<P>As with DRAM refresh, wait states don&#146;t stop the 8088 completely. The Execution Unit can continue processing while wait states are inserted, so long as the EU doesn&#146;t need to perform a bus access. However, in the PC, wait states most often occur when an instruction accesses a memory operand, so in fact the Execution Unit usually is stopped by wait states. (Instruction fetches rarely wait in an 8088-based PC because system memory is zero-wait-state. AT-class memory systems routinely insert 1 or more wait states, however.)
</P>
<P>As it turns out, wait states pose a serious problem in just one area in the PC. While any adapter <I>can</I> insert wait states, in the PC only display adapters do so to the extent that performance is seriously affected.</P>
<H3><A NAME="Heading20"></A><FONT COLOR="#000077">The Display Adapter Cycle-Eater</FONT></H3>
<P>Display adapters must serve two masters, and that creates a fundamental performance problem. Master #1 is the circuitry that drives the display screen. This circuitry must constantly read display memory in order to obtain the information used to draw the characters or dots displayed on the screen. Since the screen must be redrawn between 50 and 70 times per second, and since each redraw of the screen can require as many as 36,000 reads of display memory (more in Super VGA modes), master #1 is a demanding master indeed. No matter how demanding master #1 gets, however, its needs must <I>always</I> be met&#151;otherwise the quality of the picture on the screen would suffer.</P>
<P>Master #2 is the CPU, which reads from and writes to display memory in order to manipulate the bytes that the video circuitry reads to form the picture on the screen. Master #2 is less important than master #1, since the CPU affects display quality only indirectly. In other words, if the video circuitry has to wait for display memory accesses, the picture will develop holes, snow, and the like, but if the CPU has to wait for display memory accesses, the program will just run a bit slower&#151;no big deal.</P>
<P>It matters a great deal which master is more important, for while both the CPU and the video circuitry must gain access to display memory, only one of the two masters can read or write display memory at any one time. Potential conflicts are resolved by flat-out guaranteeing the video circuitry however many accesses to display memory it needs, with the CPU waiting for whatever display memory accesses are left over.</P>
<P>It turns out that the 8088 CPU has to do a lot of waiting, for three reasons. First, the video circuitry can take as much as about 90 percent of the available display memory access time, as shown in Figure 4.7, leaving as little as about 10 percent of all display memory accesses for the 8088. (These percentages vary considerably among the many EGA and VGA clones.)</P>
<P><A NAME="Fig7"><!-- </A><A HREF="javascript:displayWindow('images/04-07.jpg',415,342 )"> --><IMG SRC="images/04-07.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/04-07.jpg',415,342)"> --><FONT COLOR="#000077"><B>Figure 4.7</B></FONT></A>&nbsp;&nbsp;<I>Allocation of display memory access.</I>
</P>
<P>Second, because the displayed dots (or <I>pixels,</I> short for &#147;picture elements&#148;) must be drawn on the screen at a constant speed, many display adapters provide memory accesses only at fixed intervals. As a result, time can be lost while the 8088 synchronizes with the start of the next display adapter memory access, even if the video circuitry isn&#146;t accessing display memory at that time, as shown in Figure 4.8.</P>
<P>Finally, the time it takes a display adapter to complete a memory access is related to the speed of the clock which generates pixels on the screen rather than to the memory access speed of the 8088. Consequently, the time taken for display memory to complete an 8088 read or write access is often longer than the time taken for system memory to complete an access, even if the 8088 lucks into hitting a free display memory access just as it becomes available, again as shown in Figure 4.8. Any or all of the three factors I&#146;ve described can result in wait states, slowing the 8088 and creating the display adapter cycle.</P>
<P><A NAME="Fig8"><!-- </A><A HREF="javascript:displayWindow('images/04-08.jpg',417,375 )"> --><IMG SRC="images/04-08.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/04-08.jpg',417,375)"> --><FONT COLOR="#000077"><B>Figure 4.8</B></FONT></A>&nbsp;&nbsp;<I>Display memory access slots.</I>
</P>
<P>If some of this is Greek to you, don&#146;t worry. The important point is that display memory is not very fast compared to normal system memory. How slow is it? <I>Incredibly</I> slow. Remember how slow IBM&#146;s ill-fated PC<I>jr</I>was? In case you&#146;ve forgotten, I&#146;ll refresh your memory: The PC<I>jr</I>was at best only half as fast as the PC. The PC<I>jr</I> had an 8088 running at 4.77 MHz, just like the PC&#151;why do you suppose it was so much slower? I&#146;ll tell you why: <I>All the memory in the PCjr was display memory.</I></P>
<P>Enough said. All the memory in the PC is <I>not</I> display memory, however, and unless you&#146;re thickheaded enough to put code in display memory, the PC isn&#146;t going to run as slowly as a PC<I>jr.</I> (Putting code or other non-video data in unused areas of display memory sounds like a neat idea&#151;until you consider the effect on instruction prefetching of cutting the 8088&#146;s already-poor memory access performance in half. Running your code from display memory is sort of like running on a hypothetical 8084&#151;an 8086 with a <I>4-bit</I> bus. Not recommended!) Given that your code and data reside in normal system memory below the 640K mark, how great an impact does the display adapter cycle-eater have on performance?</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-07.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-09.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

131
ch04/04-09.html Normal file
View file

@ -0,0 +1,131 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: In the Lair of the Cycle-Eaters</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=4//-->
<!--PAGES=104-106//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-08.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-10.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>The answer varies considerably depending on what display adapter and what display mode we&#146;re talking about. The display adapter cycle-eater is worst with the Enhanced Graphics Adapter (EGA) and the original Video Graphics Array (VGA). (Many VGAs, especially newer ones, insert many fewer wait states than IBM&#146;s original VGA. On the other hand, Super VGAs have more bytes of display memory to be accessed in high-resolution mode.) While the Color/Graphics Adapter (CGA), Monochrome Display Adapter (MDA), and Hercules Graphics Card (HGC) all suffer from the display adapter cycle-eater as well, they suffer to a lesser degree. Since the VGA represents the base standard for PC graphics now and for the foreseeable future, and since it is the hardest graphics adapter to wring performance from, we&#146;ll restrict our discussion to the VGA (and its close relative, the EGA) for the remainder of this chapter.
</P>
<H4 ALIGN="LEFT"><A NAME="Heading21"></A><FONT COLOR="#000077">The Impact of the Display Adapter Cycle-Eater</FONT></H4>
<P>Even on the EGA and VGA, the effect of the display adapter cycle-eater depends on the display mode selected. In text mode, the display adapter cycle-eater is rarely a major factor. It&#146;s not that the cycle-eater isn&#146;t present; however, a mere 4,000 bytes control the entire text mode display, and even with the display adapter cycle-eater it just doesn&#146;t take that long to manipulate 4,000 bytes. Even if the display adapter cycle-eater were to cause the 8088 to take as much as 5&#181;s per display memory access&#151;more than five times normal&#151;it would still take only 4,000&#215; 2&#215; 5&#181;s, or 40 &#181;s, to read and write every byte of display memory. That&#146;s a lot of time as measured in 8088 cycles, but it&#146;s less than the blink of an eye in human time, and video performance only matters in human time. After all, the whole point of drawing graphics is to convey visual information, and if that information can be presented faster than the eye can see, that is by definition fast enough.
</P>
<P>That&#146;s not to say that the display adapter cycle-eater <I>can&#146;t</I> matter in text mode. In Chapter 3, I recounted the story of a debate among letter-writers to a magazine about exactly how quickly characters could be written to display memory without causing snow. The writers carefully added up Intel&#146;s instruction cycle times to see how many writes to display memory they could squeeze into a single horizontal retrace interval. (On a CGA, it&#146;s only during the short horizontal retrace interval and the longer vertical retrace interval that display memory can be accessed in 80-column text mode without causing snow.) Of course, now we know that their cardinal sin was to ignore the prefetch queue; even if there were no wait states, their calculations would have been overly optimistic. There <I>are</I> display memory wait states as well, however, so the calculations were not just optimistic but wildly optimistic.</P>
<P>Text mode situations such as the above notwithstanding, where the display adapter cycle-eater really kicks in is in graphics mode, and most especially in the high-resolution graphics modes of the EGA and VGA. The problem here is not that there are necessarily more wait states per access in highgraphics modes (that varies from adapter to adapter and mode to mode). Rather, the problem is simply that are many more bytes of display memory per screen in these modes than in lower-resolution graphics modes and in text modes, so many more display memory accesses&#151;each incurring its share of display memory wait states&#151;are required in order to draw an image of a given size. When accessing the many thousands of bytes used in the high-resolution graphics modes, the cumulative effects of display memory wait states can seriously impact code performance, even as measured in human time.</P>
<P>For example, if we assume the same 5 &#181;s per display memory access for the EGA&#146;s high-resolution graphics mode that we assumed for text mode, it would take 26,000 &#215; 2 &#215; 5 &#181;s, or 260 &#181;s, to scroll the screen once in the EGA&#146;s high-resolution graphics mode, mode 10H. That&#146;s more than one-quarter of a second&#151;noticeable by human standards, an eternity by computer standards.</P>
<P>That sounds pretty serious, but we did make an unfounded assumption about memory access speed. Let&#146;s get some hard numbers. Listing 4.11 accesses display memory at the 8088&#146;s maximum speed, by way of a <B>REP MOVSW</B> with display memory as both source and destination. The code in Listing 4.11 executes in 3.18 &#181;s per access to display memory&#151;not as long as we had assumed, but a long time nonetheless.</P>
<P><B>LISTING 4.11 LST4-11.ASM</B></P>
<!-- CODE //-->
<PRE>
; Times speed of memory access to Enhanced Graphics
; Adapter graphics mode display memory at A000:0000.
;
mov ax,0010h
int 10h; select hi-res EGA graphics
; mode 10 hex (AH=0 selects
; BIOS set mode function,
; with AL=mode to select)
;
mov ax,0a000h
mov ds,ax
mov es,ax ;move to &amp from same segment
sub si,si ;move to &amp from same offset
mov di,si
mov cx,800h ;move 2K words
cld
call ZTimerOn
rep movsw ;simply read each of the first
; 2K words of the destination segment,
; writing each byte immediately back
; to the same address. No memory
; locations are actually altered; this
; is just to measure memory access
; times
call ZTimerOff
;
mov ax,0003h
int 10h ;return to text mode
</PRE>
<!-- END CODE //-->
<P>For comparison, let&#146;s see how long the same code takes when accessing normal system RAM instead of display memory. The code in Listing 4.12, which performs a <B>REP MOVSW</B> from the code segment to the code segment, executes in 1.39 &#181;s per display memory access. That means that on average, 1.79 &#181;s (more than 8 cycles!) are lost to the display adapter cycle-eater on each access. In other words, the display adapter cycle-eater can <I>more than double</I> the execution time of 8088 code!</P>
<P><B>LISTING 4.12 LST4-12.ASM</B></P>
<!-- CODE //-->
<PRE>
; Times speed of memory access to normal system
; memory.
;
mov ax,ds
mov es,ax ;move to &amp from same segment
sub si,si ;move to &amp from same offset
mov di,si
mov cx,800h ;move 2K words
cld
call ZTimerOn
rep movsw ;simply read each of the first
; 2K words of the destination segment,
; writing each byte immediately back
; to the same address. No memory
; locations are actually altered; this
; is just to measure memory access
; times
call ZTimerOff
</PRE>
<!-- END CODE //-->
<P>Bear in mind that we&#146;re talking about a worst case here; the impact of the display adapter cycle-eater is proportional to the percent of time a given code sequence spends accessing display memory.
</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-08.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="04-10.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

91
ch04/04-10.html Normal file
View file

@ -0,0 +1,91 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: In the Lair of the Cycle-Eaters</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=04//-->
<!--PAGES=106-109//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-09.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch05/05-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<TABLE WIDTH="100%">
<TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/04-03i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>A line-drawing subroutine, which executes perhaps a dozen instructions for each display memory access, generally loses less performance to the display adapter cycle-eater than does a block-copy or scrolling subroutine that uses <B>REP MOVS</B> instructions. Scaled and three-dimensional graphics, which spend a great deal of time performing calculations (often using very slow floating-point arithmetic), tend to suffer less.</I></SMALL>
</TABLE>
<P>In addition, code that accesses display memory infrequently tends to suffer only about half of the maximum display memory wait states, because on average such code will access display memory halfway between one available display memory access slot and the next. As a result, code that accesses display memory less intensively than the code in Listing 4.11 will on average lose 4 or 5 rather than 8-plus cycles to the display adapter cycle-eater on each memory access.
</P>
<P>Nonetheless, the display adapter cycle-eater always takes its toll on graphics code. Interestingly, that toll becomes much higher on ATs and 80386 machines because while those computers can execute many more instructions per microsecond than can the 8088-based PC, it takes just as long to access display memory on those computers as on the 8088-based PC. Remember, the limited speed of access to a graphics adapter is an inherent characteristic of the adapter, so the fastest computer around can&#146;t access display memory one iota faster than the adapter will allow.</P>
<H4 ALIGN="LEFT"><A NAME="Heading22"></A><FONT COLOR="#000077">What to Do about the Display Adapter Cycle-Eater?</FONT></H4>
<P>What can we do about the display adapter cycle-eater? Well, we can minimize display memory accesses whenever possible. In particular, we can try to avoid read/modify/write display memory operations of the sort used to mask individual pixels and clip images. Why? Because read/modify/write operations require two display memory accesses (one read and one write) each time display memory is manipulated. Instead, we should try to use writes of the sort that set all the pixels in a given byte of display memory at once, since such writes don&#146;t require accompanying read accesses. The key here is that only half as many display memory accesses are required to write a byte to display memory as are required to read a byte from display memory, mask part of it off and alter the rest, and write the byte back to display memory. Half as many display memory accesses means half as many display memory wait states.
</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/04-04i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>Moreover, 486s and Pentiums, as well as recent Super VGAs, employ write-caching schemes that make display memory writes considerably faster than display memory reads.</I></SMALL>
</TABLE>
<P>Along the same line, the display adapter cycle-eater makes the popular exclusive-OR animation technique, which requires paired reads and writes of display memory, less-than-ideal for the PC. Exclusive-OR animation should be avoided in favor of simply writing images to display memory whenever possible.
</P>
<P>Another principle for display adapter programming on the 8088 is to perform multiple accesses to display memory very rapidly, in order to make use of as many of the scarce accesses to display memory as possible. This is especially important when many large images need to be drawn quickly, since only by using virtually every available display memory access can many bytes be written to display memory in a short period of time. Repeated string instructions are ideal for making maximum use of display memory accesses; of course, repeated string instructions can only be used on whole bytes, so this is another point in favor of modifying display memory a byte at a time. (On faster processors, however, display memory is so slow that it often pays to do several instructions worth of work between display memory accesses, to take advantage of cycles that would otherwise be wasted on the wait states.)</P>
<P>It would be handy to explore the display adapter cycle-eater issue in depth, with lots of example code and execution timings, but alas, I don&#146;t have the space for that right now. For the time being, all you really need to know about the display adapter cycle-eater is that on the 8088 you can lose more than 8 cycles of execution time on each access to display memory. For intensive access to display memory, the loss really can be as high as 8cycles (and up to 50, 100, or even more on 486s and Pentiums paired with slow VGAs), while for average graphics code the loss is closer to 4 cycles; in either case, the impact on performance is significant. There is only one way to discover just how significant the impact of the display adapter cycle-eater is for any particular graphics code, and that is of course to measure the performance of that code.</P>
<H4 ALIGN="LEFT"><A NAME="Heading23"></A><FONT COLOR="#000077">Cycle-Eaters: A Summary</FONT></H4>
<P>We&#146;ve covered a great deal of sophisticated material in this chapter, so don&#146;t feel bad if you haven&#146;t understood everything you&#146;ve read; it will all become clear from further reading, especially once you study, time, and tune code that you have written yourself. What&#146;s really important is that you come away from this chapter understanding that on the 8088:
</P>
<DL>
<DD><B>&#149;</B>&nbsp;&nbsp;The 8-bit bus cycle-eater causes each access to a word-sized operand to be 4 cycles longer than an equivalent access to a byte-sized operand.
<DD><B>&#149;</B>&nbsp;&nbsp;The prefetch queue cycle-eater can cause instruction execution times to be as much as four times longer than the officially documented cycle times.
<DD><B>&#149;</B>&nbsp;&nbsp;The DRAM refresh cycle-eater slows most PC code, with performance reductions ranging as high as 8.33 percent.
<DD><B>&#149;</B>&nbsp;&nbsp;The display adapter cycle-eater typically doubles and can more than triple the length of the standard 4-cycle access to display memory, with intensive display memory access suffering most.
</DL>
<P>This basic knowledge about cycle-eaters puts you in a good position to understand the results reported by the Zen timer, and that means that you&#146;re well on your way to writing high-performance assembler code.
</P>
<H4 ALIGN="LEFT"><A NAME="Heading24"></A><FONT COLOR="#000077">What Does It All Mean?</FONT></H4>
<P>There you have it: life under the programming interface. It&#146;s not a particularly pretty picture for the inhabitants of that strange realm where hardware and software meet are little-known cycle-eaters that sap the speed from your unsuspecting code. Still, some of those cycle-eaters can be minimized by keeping instructions short, using the registers, using byte-sized memory operands, and accessing display memory as little as possible. None of the cycle-eaters can be eliminated, and dynamic RAM refresh can scarcely be addressed at all; still, aren&#146;t you better off knowing how fast your code <I>really</I> runs&#151;and why&#151;than you were reading the official execution times and guessing? And while specific cycle-eaters vary in importance on later x86-family processors, with some cycle-eaters vanishing altogether and new ones appearing, the concept that understanding these obscure gremlins is a key to performance remains unchanged, as we&#146;ll see again and again in later chapters.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="04-09.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch05/05-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

BIN
ch04/images/04-01.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
ch04/images/04-01i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch04/images/04-02.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
ch04/images/04-02i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch04/images/04-03.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
ch04/images/04-03i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch04/images/04-04.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
ch04/images/04-04i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch04/images/04-05.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
ch04/images/04-06.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
ch04/images/04-07.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
ch04/images/04-08.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

85
ch05/05-01.html Normal file
View file

@ -0,0 +1,85 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Crossing the Border</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=05//-->
<!--PAGES=111-115//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch04/04-10.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="05-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H2><A NAME="Heading1"></A><FONT COLOR="#000077">Chapter 5<BR>Crossing the Border
</FONT></H2>
<H3><A NAME="Heading2"></A><FONT COLOR="#000077">Searching Files with Restartable Blocks</FONT></H3>
<P><I>We just moved.</I> Those three little words should strike terror into the heart of anyone who owns more than a sleeping bag and a toothbrush. Our last move was the usual zoo&#151;and then some. Because the distance from the old house to the new was only five miles, we used cars to move everything smaller than a washing machine. We have a sizable household&#151;cats, dogs, kids, com, you name it&#151;so the moving process took a number of car trips. A <I>large</I> number&#151;33, to be exact. I personally spent about 15 hours just driving back and forth between the two houses. The move took days to complete.</P>
<P><I>Never again</I>.</P>
<P>You&#146;re probably wondering two things: What does this have to do with high-performance programming, and why on earth didn&#146;t I rent a truck and get the move over in one or two trips, saving hours of driving? As it happens, the second question answers the first. I didn&#146;t rent a truck because it <I>seemed</I> easier and cheaper to use cars&#151;no big truck to drive, no rentals, spread the work out more manageably, and so on.</P>
<P>It wasn&#146;t easier, and wasn&#146;t even much cheaper. (It costs quite a bit to drive a car 330 miles, to say nothing of the value of 15 hours of my time.) But, at the time, it seemed as though my approach would be easier and cheaper. In fact, I didn&#146;t realize just how much time I had wasted driving back and forth until I sat down to write this chapter.</P>
<P>In Chapter 1, I briefly discussed using <I>restartable blocks</I>. This, you might remember, is the process of handling in chunks data sets too large to fit in memory so that they can be processed just about as fast as if they did fit in memory. The restartable block approach is very fast but is relatively difficult to program.</P>
<P>At the opposite end of the spectrum lies byte-by-byte processing, whereby DOS (or, in less extreme cases, a group of library functions) is allowed to do all the hard work, so that you only have to deal with one byte at a time. Byte-by-byte processing is easy to program but can be extremely slow, due to the vast overhead that results from invoking DOS each time a byte must be processed.</P>
<P>Sound familiar? It should. I moved via the byte-by-byte approach, and the overhead of driving back and forth made for miserable performance. Renting a truck (the restartable block approach) would have required more effort and forethought, but would have paid off handsomely.</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/05-01i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>The easy, familiar approach often has nothing in its favor except that it requires less thinking; not a great virtue when writing high-performance code&#151;or when moving.</I></SMALL>
</TABLE>
<P>And with that, let&#146;s look at a fairly complex application of restartable blocks.
</P>
<H4 ALIGN="LEFT"><A NAME="Heading3"></A><FONT COLOR="#000077">Searching for Text</FONT></H4>
<P>The application we&#146;re going to examine searches a file for a specified string. We&#146;ll develop a program that will search the file specified on the command line for a string (also specified on the comline), then report whether the string was found or not. (Because the searched-for string is obtained via <B>argv</B>, it can&#146;t contain any whitespace characters.)</P>
<P>This is a <I>very</I> limited subset of what search utilities such as grep can do, and isn&#146;t really intended to be a generally useful application; the purpose is to provide insight into restartable blocks in particular and optimization in general in the course of developing a search engine. That search engine will, however, be easy to plug into any program, and there&#146;s nothing preventing you from using it in a more fruitful context, like searching through a user-selectable file set.</P>
<P>The first point to address in designing our program involves the appropriate text-search approach to use. Literally dozens of workable ways exist to search a file. We can immediately discard all approaches that involve reading any byte of the file more than once, because disk access time is orders of magnitude slower than any data handling performed by our own code. Based on our experience in Chapter 1, we can also discard all approaches that get bytes either one at a time or in small sets from DOS. We want to read big &#147;buffers-full&#148; of bytes at a pop from the searched file, and the bigger the buffer the better&#151;in order to minimize DOS&#146;s overhead. A good rough cut is a buffer that will be between 16K and 64K, depending on the exact search approach, 64K being the maximum size because near pointers make for superior performance.</P>
<P>So we know we want to work with a large buffer, filling it as infrequently as possible. Now we have to figure out how to search through a file by loading it into that large buffer in chunks. To accomplish this, we have to know how we want to do our searching, and that&#146;s not immediately obvious. Where do we begin?</P>
<P>Well, it might be instructive to consider how we would search if our search involved only one buffer, already resident in memory. In other words, suppose we don&#146;t have to bother with file handling at all, and further suppose that we don&#146;t have to deal with searching through multiple blocks. After all, that&#146;s a good description of the all-important inner loop of our searching program, where the program will spend virtually all of its time (aside from the unavoidable disk access overhead).</P>
<H3><A NAME="Heading4"></A><FONT COLOR="#000077">Avoiding the String Trap</FONT></H3>
<P>The easiest approach would be to use a C/C<SMALL>&#43;&#43;</SMALL> library function. The closest match to what we need is <B>strstr()</B>, which searches one string for the first occurrence of a second string. However, while <B>strstr()</B> would work, it isn&#146;t ideal for our purposes. The problem is this: Where we want to search a fixed-length buffer for the first occurrence of a string, <B>strstr()</B> searches a <I>string</I> for the first occurrence of another string.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch04/04-10.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="05-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

88
ch05/05-02.html Normal file
View file

@ -0,0 +1,88 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Crossing the Border</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=05//-->
<!--PAGES=115-118//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="05-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="05-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>We could put a zero byte at the end of our buffer to allow <B>strstr()</B> to work, but why bother? The <B>strstr()</B> function must spend time either checking for the end of the string being searched or determining the length of that string&#151;wasted effort given that we already know exactly how long our search buffer is. Even if a given <B>strstr()</B> implementation is well-written, its performance will suffer, at least for our application, from unnecessary overhead.</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/05-02i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>This illustrates why you shouldn&#146;t think of C/C<SMALL>&#43;&#43;</SMALL> library functions as black boxes; understand what they do and try to figure out how they do it, and relate that to their performance in the context you&#146;re interested in.</I></SMALL>
</TABLE>
<H3><A NAME="Heading5"></A><FONT COLOR="#000077">Brute-Force Techniques</FONT></H3>
<P>Given that no C/C<SMALL>&#43;&#43;</SMALL> library function meets our needs precisely, an obvious alternative approach is the brute-force technique that uses <B>memcmp()</B> to compare <I>every</I> potential matching location in the buffer to the string we&#146;re searching for, as illustrated in Figure 5.1.</P>
<P>By the way, we could, of course, use our own code, working with pointers in a loop, to perform the comparison in place of <B>memcmp()</B>. But <B>memcmp()</B> will almost certainly use the very fast <B>REPZ CMPS</B> instruction. However, <I>never assume!</I> It wouldn&#146;t hurt to use a debugger to check out the actual machine-code implementation of <B>memcmp()</B> from your compiler. If necessary, you could always write your own assembly language implementation of <B>memcmp()</B>.</P>
<P><A NAME="Fig1"><!-- </A><A HREF="javascript:displayWindow('images/05-01.jpg',415,211 )"> --><IMG SRC="images/05-01.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/05-01.jpg',415,211)"> --><FONT COLOR="#000077"><B>Figure 5.1</B></FONT></A>&nbsp;&nbsp;<I>The brute-force searching technique.</I>
</P>
<P>Invoking <B>memcmp()</B> for each potential match location works, but entails considerable overhead. Each comparison requires that parameters be pushed and that a call to and return from <B>memcmp()</B> be performed, along with a pass through the comparison loop. Surely there&#146;s a better way!</P>
<P>Indeed there is. We can eliminate most calls to <B>memcmp()</B> by performing a simple test on each potential match location that will reject most such locations right off the bat. We&#146;ll just check whether the first character of the potentially matching buffer location matches the first character of the string we&#146;re searching for. We could make this check by using a pointer in a loop to scan the buffer for the next match for the first character, stopping to check for a match with the rest of the string <I>only</I> when the first character matches, as shown in Figure 5.2.</P>
<H3><A NAME="Heading6"></A><FONT COLOR="#000077">Using memchr()</FONT></H3>
<P>There&#146;s yet a better way to implement this approach, however. Use the <B>memchr()</B> function, which does nothing more or less than find the next occurrence of a specified character in a fixed-length buffer (presumably by using the extremely efficient <B>REPNZ SCASB</B> instruction, although again it wouldn&#146;t hurt to check). By using <B>memchr()</B> to scan for potential matches that can then be fully tested with <B>memcmp()</B>, we can build a highly efficient search engine that takes good advantage of the information we have about the buffer being searched and the string we&#146;re searching for. Our engine also relies heavily on repeated string instructions, assuming that the <B>memchr()</B> and <B>memcmp()</B> library functions are properly coded.</P>
<P><A NAME="Fig2"><!-- </A><A HREF="javascript:displayWindow('images/05-02.jpg',409,241 )"> --><IMG SRC="images/05-02.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/05-02.jpg',409,241)"> --><FONT COLOR="#000077"><B>Figure 5.2</B></FONT></A>&nbsp;&nbsp;<I>The faster string-searching technique.</I>
</P>
<P>We&#146;re going to go with the this approach in our file-searching program; the only trick lies in deciding how to integrate this approach with restartable blocks in order to search through files larger than our buffer. This certainly isn&#146;t the fastest-possible searching algorithm; as one example, the Boyer-Moore algorithm, which cleverly eliminates many buffer locations as potential matches in the process of checking preceding locations, can be considerably faster. However, the Boyer-Moore algorithm is quite complex to understand and implement, and would distract us from our main focus, restartable blocks, so we&#146;ll save it for a later chapter (Chapter 14, to be precise). Besides, I suspect you&#146;ll find the approach we&#146;ll use to be fast enough for most purposes.
</P>
<P>Now that we&#146;ve selected a searching approach, let&#146;s integrate it with file handling and searching through multiple blocks. In other words, let&#146;s make it restartable.</P>
<H4 ALIGN="LEFT"><A NAME="Heading7"></A><FONT COLOR="#000077">Making a Search Restartable</FONT></H4>
<P>As it happens, there&#146;s no great trick to putting the pieces of this search program together. Basically, we&#146;ll read in a buffer of data (we&#146;ll work with 16K at a time to avoid signed overflow problems with integers), search it for a match with the <B>memchr()/memcmp()</B> engine described, and exit with a &#147;string found&#148; response if the desired string is found.</P>
<P>Otherwise, we&#146;ll load in another buffer full of data from the file, search it, and so on. The only trick lies in handling potentially matching sequences in the file that start in one buffer and end in the next&#151;that is, sequences that span buffers. We&#146;ll handle this by copying the unchecked bytes at the end of one buffer to the start of the next and reading that many fewer bytes the next time we fill the buffer.</P>
<P>The exact number of bytes to be copied from the end of one buffer to the start of the next is the length of the searched-for string minus 1, since that&#146;s how many bytes at the end of the buffer can&#146;t be checked as possible matches (because the check would run off the end of the buffer).</P>
<P>That&#146;s really all there is to it. Listing 5.1 shows the file-searching program. As you can see, it&#146;s not particularly complex, although a few fairly opaque lines of code are required to handle merging the end of one block with the start of the next. The code that searches a single block&#151;the function <B>SearchForString()&#151;</B>is simple and compact (as it should be, given that it&#146;s by far the most heavily-executed code in the listing).</P>
<P>Listing 5.1 nicely illustrates the core concept of restartable blocks: Organize your program so that you can do your processing within each block as fast as you could if there were only one block&#151;which is to say at top speed&#151;and make your blocks as large as possible in order to minimize the overhead associated with going from one block to the next.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="05-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="05-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

234
ch05/05-03.html Normal file
View file

@ -0,0 +1,234 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Crossing the Border</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=05//-->
<!--PAGES=118-121//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="05-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="05-04.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P><B>LISTING 5.1 SEARCH.C</B></P>
<!-- CODE //-->
<PRE>
/* Program to search the file specified by the first command-line
* argument for the string specified by the second command-line
* argument. Performs the search by reading and searching blocks
* of size BLOCK_SIZE. */
#include &ltstdio.h&gt
#include &ltfcntl.h&gt
#include &ltstring.h&gt
#include &ltalloc.h&gt /* alloc.h for Borland compilers,
malloc.h for Microsoft compilers */
#define BLOCK_SIZE 0x4000 /* we&#146;ll process the file in 16K blocks */
/* Searches the specified number of sequences in the specified
buffer for matches to SearchString of SearchStringLength. Note
that the calling code should already have shortened SearchLength
if necessary to compensate for the distance from the end of the
buffer to the last possible start of a matching sequence in the
buffer.
*/
int SearchForString(unsigned char *Buffer, int SearchLength,
unsigned char *SearchString, int SearchStringLength)
{
unsigned char *PotentialMatch;
/* Search so long as there are potential-match locations
remaining */
while ( SearchLength ) {
/* See if the first character of SearchString can be found */
if ( (PotentialMatch =
memchr(Buffer, *SearchString, SearchLength)) == NULL ) {
break; /* No matches in this buffer */
}
/* The first character matches; see if the rest of the string
also matches */
if ( SearchStringLength == 1 ) {
return(1); /* That one matching character was the whole
search string, so we&#146;ve got a match */
}
else {
/* Check whether the remaining characters match */
if ( !memcmp(PotentialMatch &#43; 1, SearchString &#43; 1,
SearchStringLength - 1) ) {
return(1); /* We&#146;ve got a match */
}
}
/* The string doesn&#146;t match; keep going by pointing past the
potential match location we just rejected */
SearchLength -= PotentialMatch - Buffer &#43; 1;
Buffer = PotentialMatch &#43; 1;
}
return(0); /* No match found */
}
main(int argc, char *argv[]) {
int Done; /* Indicates whether search is done */
int Handle; /* Handle of file being searched */
int WorkingLength; /* Length of current block */
int SearchStringLength; /* Length of string to search for */
int BlockSearchLength; /* Length to search in current block */
int Found; /* Indicates final search completion
status */
int NextLoadCount; /* # of bytes to read into next block,
accounting for bytes copied from the
last block */
unsigned char *WorkingBlock; /* Block storage buffer */
unsigned char *SearchString; /* Pointer to the string to search for */
unsigned char *NextLoadPtr; /* Offset at which to start loading
the next block, accounting for
bytes copied from the last block */
/* Check for the proper number of arguments */
if ( argc != 3 ) {
printf(&#147;usage: search filename search-string\n&#148;);
exit(1);
}
/* Try to open the file to be searched */
if ( (Handle = open(argv[1], O_RDONLY | O_BINARY)) == -1 ) {
printf(&#147;Can&#146;t open file: %s\n&#148;, argv[1]);
exit(1);
}
/* Calculate the length of text to search for */
SearchString = argv[2];
SearchStringLength = strlen(SearchString);
/* Try to get memory in which to buffer the data */
if ( (WorkingBlock = malloc(BLOCK_SIZE)) == NULL ) {
printf(&#147;Can&#146;t get enough memory\n&#148;);
exit(1);
}
/* Load the first block at the start of the buffer, and try to
fill the entire buffer */
NextLoadPtr = WorkingBlock;
NextLoadCount = BLOCK_SIZE;
Done = 0; /* Not done with search yet */
Found = 0; /* Assume we won&#146;t find a match */
/* Search the file in BLOCK_SIZE chunks */
do {
/* Read in however many bytes are needed to fill out the block
(accounting for bytes copied over from the last block), or
the rest of the bytes in the file, whichever is less */
if ( (WorkingLength = read(Handle, NextLoadPtr,
NextLoadCount)) == -1 ) {
printf(&#147;Error reading file %s\n&#148;, argv[1]);
exit(1);
}
/* If we didn&#146;t read all the bytes we requested, we&#146;re done
after this block, whether we find a match or not */
if ( WorkingLength != NextLoadCount ) {
Done = 1;
}
/* Account for any bytes we copied from the end of the last
block in the total length of this block */
WorkingLength &#43;= NextLoadPtr - WorkingBlock;
/* Calculate the number of bytes in this block that could
possibly be the start of a matching sequence that lies
entirely in this block (sequences that run off the end of
the block will be transferred to the next block and found
when that block is searched)
*/
if ( (BlockSearchLength =
WorkingLength - SearchStringLength &#43; 1) &lt= 0 ) {
Done = 1; /* Too few characters in this block for
there to be any possible matches, so this
is the final block and we&#146;re done without
finding a match
*/
}
else {
/* Search this block */
if ( SearchForString(WorkingBlock, BlockSearchLength,
SearchString, SearchStringLength) ) {
Found = 1; /* We&#146;ve found a match */
Done = 1;
}
else {
/* Copy any bytes from the end of the block that start
potentially-matching sequences that would run off
the end of the block over to the next block */
if ( SearchStringLength &gt 1 ) {
memcpy(WorkingBlock,
WorkingBlock&#43;BLOCK_SIZE - SearchStringLength &#43; 1,
SearchStringLength - 1);
}
/* Set up to load the next bytes from the file after the
bytes copied from the end of the current block */
NextLoadPtr = WorkingBlock &#43; SearchStringLength - 1;
NextLoadCount = BLOCK_SIZE - SearchStringLength &#43; 1;
}
}
} while ( !Done );
/* Report the results */
if ( Found ) {
printf(&#147;String found\n&#148;);
} else {
printf(&#147;String not found\n&#148;);
}
exit(Found); /* Return the found/not found status as the
DOS errorlevel */
}
</PRE>
<!-- END CODE //-->
<P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="05-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="05-04.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

76
ch05/05-04.html Normal file
View file

@ -0,0 +1,76 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Crossing the Border</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=05//-->
<!--PAGES=121-122//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="05-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="05-05.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H3><A NAME="Heading8"></A><FONT COLOR="#000077">Interpreting Where the Cycles Go</FONT></H3>
<P>To boost the overall performance of Listing 5.1, I would normally convert <B>SearchForString()</B> to assembly language at this point. However, I&#146;m not going to do that, and the reason is as important a lesson as any discussion of optimized assembly code is likely to be. Take a moment to examine some interesting performance aspects of the C implementation, and all should become much clearer.</P>
<P>As you&#146;ll recall from Chapter 1, one of the important rules for optimization involves knowing when optimization is worth bothering with at all. Another rule involves understanding where most of a program&#146;s execution time is going. That&#146;s more true for Listing 5.1 than you might think.</P>
<P>When Listing 5.1 is run on a 1 MB assembly source file, it takes about three seconds to find the string &#147;xxxend&#148; (which is at the end of the file) on a 20 MHz 386 machine, with the entire file in a disk cache. If <B>BLOCK_SIZE</B> is trimmed from 16K to 4K, <I>execution time does not increase perceptibly!</I> At 2K, the program slows slightly; it&#146;s not until the block size shrinks to 64 bytes that execution time becomes approximately double that of the 16K buffer.</P>
<P>So the first thing we&#146;ve discovered is that, while bigger blocks do make for the best performance, the increment in performance may not be very large, and might not justify the extra memory required for those larger blocks. Our next discovery is that, even though we read the file in large chunks, most of the execution time of Listing 5.1 is nonetheless spent in executing the <B>read()</B> function.</P>
<P>When I replaced the <B>read()</B> function call in Listing 5.1 with code that simply fools the program into thinking that a 1 MB file is being read, the program ran almost instantaneously&#151;in less than 1/2 second, even when the searched-for string wasn&#146;t anywhere to be found. By contrast, Listing 5.1 requires three seconds to run even when searching for a single character that isn&#146;t found anywhere in the file, the case in which a single call to <B>memchr()</B> (and thus a single <B>REPNZ SCASB</B>) can eliminate an entire block at a time.</P>
<P>All in all, the time required for DOS disk access calls is taking up at least 80 percent of execution time, and search time is less than 20 percent of overall execution time. In fact, search time is probably a good deal less than 20 percent of the total, given that the overhead of loading the program, running through the C startup code, opening the file, executing <B>printf()</B>, and exiting the program and returning to the DOS shell are also included in my timings. Given which, it should be apparent why converting to assembly language isn&#146;t worth the trouble&#151;the best we could do by speeding up the search is a 10 percent or so improvement, and that would require more than doubling the performance of code that already uses repeated string instructions to do most of the work.</P>
<P>Not likely.</P>
<H4 ALIGN="LEFT"><A NAME="Heading9"></A><FONT COLOR="#000077">Knowing When Assembly Is Pointless</FONT></H4>
<P>So that&#146;s why we&#146;re not going to go to assembly language in this example&#151;which is not to say it would never be worth converting the search engine in Listing 5.1 to assembly.
</P>
<P>If, for example, your application will typically search buffers in which the first character of the search string occurs frequently as might be the case when searching a text buffer for a string starting with the space character an assembly implementation might be several times faster. Why? Because assembly code can switch from <B>REPNZ SCASB</B> to match the first character to <B>REPZ CMPS</B> to check the remaining characters in just a few instructions.</P>
<P>In contrast, Listing 5.1 must return from <B>memchr()</B>, set up parameters, and call <B>memcmp()</B> in order to do the same thing. Likewise, assembly can switch back to <B>REPNZ SCASB</B> after a non-match much more quickly than Listing 5.1. The switching overhead is high; when searching a file completely filled with the character z for the string &#147;zy,&#148; Listing 5.1 takes almost 1/2 minute, or nearly an order of magnitude longer than when searching a file filled with normal text.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="05-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="05-05.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

81
ch05/05-05.html Normal file
View file

@ -0,0 +1,81 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Crossing the Border</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=05//-->
<!--PAGES=122-123//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="05-04.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch06/06-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>It might also be worth converting the search engine to assembly for searches performed entirely in memory; with the overhead of file access eliminated, improvements in search-engine performance would translate directly into significantly faster overall performance. One such application that would have much the same structure as Listing 5.1 would be searching through expanded memory buffers, and another would be searching through huge (segment-spanning) buffers.
</P>
<P>And so we find, as we so often will, that optimization is definitely not a cut-and-dried matter, and that there is no such thing as a single &#147;best&#148; approach.</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/05-03i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>You must know what your application will typically do, and you must know whether you&#146;re more concerned with average or worst-case performance before you can decide how best to speed up your program&#151;and, indeed, whether speeding it up is worth doing at all.</I></SMALL>
</TABLE>
<P>By the way, don&#146;t think that just because very large block sizes don&#146;t much improve performance, it wasn&#146;t worth using restartable blocks in Listing 5.1. Listing 5.1 runs more than three times more slowly with a block size of 32 bytes than with a block size of 4K, and any byte-by-byte approach would surely be slower still, due to the overhead of repeated calls to DOS and/or the C stream I/O library.
</P>
<P>Restartable blocks do minimize the overhead of DOS file-access calls in Listing 5.1; it&#146;s just that there&#146;s no way to reduce that overhead to the point where it becomes worth attempting to further improve the performance of our relatively efficient search engine. Although the search engine is by no means fully optimized, it&#146;s nonetheless as fast as there&#146;s any reason for it to be, given the balance of performance among the components of this program.</P>
<H3><A NAME="Heading10"></A><FONT COLOR="#000077">Always Look Where Execution Is Going</FONT></H3>
<P>I&#146;ve explained two important lessons: Know when it&#146;s worth optimizing further, and use restartable blocks to process large data sets as a series of blocks, with each block handled at high speed. The first lesson is less obvious than it seems.
</P>
<P>When I set out to write this chapter, I fully intended to write an assembly language version of Listing 5.1, and I expected the assembly version to be much faster. When I actually looked at where execution time was going (which I did by modifying the program to remove the calls to the <B>read()</B> function, but a code profiler could be used to do the same thing much more easily), I found that the best code in the world wouldn&#146;t make much difference.</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/05-04i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>When you try to speed up code, take a moment to identify the hot spots in your program so that you know where optimization is needed and whether it will make a significant difference before you invest your time.</I></SMALL>
</TABLE>
<P>As for restartable blocks: Here we tackled a considerably more complex application of restartable blocks than we did in Chapter 1&#151;which turned out not to be so difficult after all. Don&#146;t let irregularities in the programming tasks you tackle, such as strings that span blocks, fluster you into settling for easy, general&#151;and slow&#151;solutions. Focus on making the inner loop&#151;the code that handles each block&#151;as efficient as possible, then structure the rest of your code to support the inner loop.
</P>
<P>Programming with restartable blocks isn&#146;t easy, but when speed is an issue, using restartable blocks in the right places more than pays for itself with greatly improved performance. And when speed is <I>not</I> an issue, of course, or in code that&#146;s not time-critical, you wouldn&#146;t dream of wasting your time on optimization.</P>
<P>Would you?</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="05-04.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch06/06-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

BIN
ch05/images/05-01.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
ch05/images/05-01i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch05/images/05-02.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
ch05/images/05-02i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch05/images/05-03i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch05/images/05-04i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

102
ch06/06-01.html Normal file
View file

@ -0,0 +1,102 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Looking Past Face Value</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=06//-->
<!--PAGES=125-129//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch05/05-05.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="06-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H2><A NAME="Heading1"></A><FONT COLOR="#000077">Chapter 6<BR>Looking Past Face Value
</FONT></H2>
<H3><A NAME="Heading2"></A><FONT COLOR="#000077">How Machine Instructions May Do More Than You Think</FONT></H3>
<P>I first met Jeff Duntemann at an authors&#146; dinner hosted by <I>PC Tech Journal</I> at Fall Comdex, back in 1985. Jeff was already reasonably well-known as a computer editor and writer, although not as famous as <I>Complete Turbo Pascal</I>, editions 1 through 672 (or thereabouts), <I>TURBO TECHNIX</I>, and <I>PC TECHNIQUES</I> would soon make him. I was fortunate enough to be seated next to Jeff at the dinner table, and, not surprisingly, our often animated conversation revolved around computers, computer writing, and more computers (not necessarily in that order).</P>
<P>Although I was making a living at computer work and enjoying it at the time, I nonetheless harbored vague ambitions of being a science-fiction writer when I grew up. (I have since realized that this hardly puts me in elite company, especially in the computer world, where it seems that every other person has told me they plan to write science fiction &#147;someday.&#148; Given that probably fewer than 500&#151;I&#146;m guessing here&#151;original science fiction and fantasy short stories, and perhaps a few more novels than that, are published each year in this country, I see a few mid-life crises coming.)</P>
<P>At any rate, I had accumulated a small collection of rejection slips, and fancied myself something of an old hand in the field. At the end of the dinner, as the other writers complained half-seriously about how little they were paid for writing for <I>Tech Journal</I>, I leaned over to Jeff and whispered, &#147;You know, the pay isn&#146;t so bad here. You should see what they pay for science fiction&#151;even to the guys who win awards!&#148;</P>
<P>To which Jeff replied, &#147;I know. I&#146;ve been nominated for two Hugos.&#148;</P>
<P>Oh.</P>
<P>Had I known I was seated next to a real, live science-fiction writer&#151;an <I>award-nominated</I> writer, by God!&#151;I would have pumped him for all I was worth, but the possibility had never occurred to me. I was at a dinner put on by a computer magazine, seated next to an editor who had just finished a book about Turbo Pascal, and, gosh, it was <I>obvious</I> that the appropriate topic was computers.</P>
<P>For once, the moral is <I>not</I> &#147;don&#146;t judge a book by its cover.&#148; Jeff is in fact what he appeared to be at face value: a computer writer and editor. However, he is more, too; face value wasn&#146;t full value. You&#146;ll similarly find that face value isn&#146;t always full value in computer programming, and especially so when working in assembly language, where many instructions have talents above and beyond their obvious abilities.</P>
<P>On the other hand, there are also a number of instructions, such as <B>LOOP</B>, that are designed to perform specific functions but aren&#146;t always the best instructions for those functions. So don&#146;t judge a book by its cover, either.</P>
<P>Assembly language for the x86 family isn&#146;t like any other language (for which we should, without hesitation, offer our profuse thanks). Assembly language reflects the design of the processor rather than the way we think, so it&#146;s full of multiple instructions that perform similar functions, instructions with odd and often confusing side effects, and endless ways to string together different instructions to do much the same things, often with seemingly minuscule differences that can turn out to be surprisingly important.</P>
<P>To produce the best code, you must decide precisely what you need to accomplish, then put together the sequence of instructions that accomplishes that end most efficiently, regardless of what the instructions are usually used for. That&#146;s why optimization for the PC is an art, and it&#146;s why the best assembly language for the x86 family will almost always handily outperform compiled code. With that in mind, let&#146;s look past face value&#151;and while we&#146;re at it, I&#146;ll toss in a few examples of not judging a book by its cover.</P>
<P>The point to all this: You must come to regard the x86 family instructions for what they do, not what you&#146;re used to thinking they do. Yes, <B>SHL</B> shifts a pattern left&#151;but a look-up table can do the same thing, and can often do it faster. <B>ADD</B> can indeed add two operands, but it can&#146;t put the result in a third register; <B>LEA</B> can. The instruction set is your raw material for writing high-performance code. By limiting yourself to thinking only in certain well-established ways about the various instructions, you&#146;re putting yourself at a substantial disadvantage every time you sit down to program.</P>
<P>In short, the x86 family can do much more than you think&#151;if you&#146;ll use everything it has to offer. Give it a shot!</P>
<H4 ALIGN="LEFT"><A NAME="Heading3"></A><FONT COLOR="#000077">Memory Addressing and Arithmetic</FONT></H4>
<P>Years ago, I saw a clip on the David Letterman show in which Letterman walked into a store by the name of &#147;Just Lamps&#148; and asked, &#147;So what do you sell here?&#148;
</P>
<P>&#147;Lamps,&#148; he was told. &#147;Just lamps. Can&#146;t you read?&#148;</P>
<P>&#147;Lamps,&#148; he said. &#147;I see. And what else?&#148;</P>
<P>From that bit of sublime idiocy we can learn much about divining the full value of an instruction. To wit:</P>
<P>Quick, what do the x86&#146;s memory addressing modes do?</P>
<P>&#147;Calculate memory addresses,&#148; you no doubt replied. And you&#146;re right, of course. But what <I>else</I> do they do?</P>
<P>They perform arithmetic, that&#146;s what they do, and that&#146;s a distinctly different and often useful perspective on memory address calculations.</P>
<P>For example, suppose you have an array base address in BX and an index into the array in SI. You could add the two registers together to address memory, like this:</P>
<!-- CODE SNIP //-->
<PRE>
add bx,si
mov al,[bx]
</PRE>
<!-- END CODE SNIP //-->
<P>Or you could let the processor do the arithmetic for you in a single instruction:
</P>
<!-- CODE SNIP //-->
<PRE>
mov al,[bx&#43;si]
</PRE>
<!-- END CODE SNIP //-->
<P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch05/05-05.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="06-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

162
ch06/06-02.html Normal file
View file

@ -0,0 +1,162 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Looking Past Face Value</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=06//-->
<!--PAGES=129-133//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="06-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch07/07-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>The two approaches are functionally interchangeable but <I>not</I> equivalent from a performance standpoint, and which is better depends on the particular context. If it&#146;s a one-shot memory access, it&#146;s best to let the processor perform the addition; it&#146;s generally faster at doing this than a separate <B>ADD</B> instruction would be. If it&#146;s a memory access within a loop, however, it&#146;s advantageous on the 8088 CPU to perform the addition outside the loop, if possible, reducing effective address calculation time inside the loop, as in the following:</P>
<!-- CODE SNIP //-->
<PRE>
add bx,si
LoopTop:
mov al,[bx]
inc bx
loop LoopTop
</PRE>
<!-- END CODE SNIP //-->
<P>Here, <B>MOV AL,[BX]</B> is two cycles faster than <B>MOV AL,[BX&#43;SI]</B>.</P>
<P>On a 286 or 386, however, the balance shifts. <B>MOV AL,[BX&#43;SI]</B> takes no longer than <B>MOV AL,[BX]</B> on these processors because effective address calculations generally take no extra time at all. (According to the MASM manual, one extra clock is required if three memory addressing components, as in <B>MOV AL,[BX&#43;SI&#43;1]</B>, are used. I have not been able to confirm this from Intel publications, but then I haven&#146;t looked all that hard.) If you&#146;re optimizing for the 286 or 386, then, you can take advantage of the processor&#146;s ability to perform arithmetic as part of memory address calculations without taking a performance hit.</P>
<P>The 486 is an odd case, in which the use of an index register or the use of a base register that&#146;s the destination of the previous instruction may slow things down, so it is generally but not always better to perform the addition outside the loop on the 486. All memory addressing calculations are free on the Pentium, however. I&#146;ll discuss 486 performance issues in Chapters 12 and 13, and the Pentium in Chapters 19 through 21.</P>
<H3><A NAME="Heading4"></A><FONT COLOR="#000077">Math via Memory Addressing</FONT></H3>
<P>You&#146;re probably not particularly wowed to hear that you can use addressing modes to perform memory addressing arithmetic that would otherwise have to be performed with separate arithmetic instructions. You may, however, be a tad more interested to hear that you can also use addressing modes to perform arithmetic that has nothing to do with memory addressing, and with a couple of advantages over arithmetic instructions, at that.
</P>
<P>How?</P>
<P>With <B>LEA</B>, the only instruction that performs memory addressing calculations but doesn&#146;t actually address memory. <B>LEA</B> accepts a standard memory addressing operand, but does nothing more than store the calculated memory offset in the specified register, which may be any general-purpose register. The operation of <B>LEA</B> is illustrated in Figure 6.1, which also shows the operation of register-to-register <B>ADD</B>, for comparis on.</P>
<P>What does that give us? Two things that <B>ADD</B> doesn&#146;t provide: the ability to perform addition with either two or three operands, and the ability to store the result in <I>any</I> register, not just in one of the source operands.</P>
<P>Imagine that we want to add BX to DI, add two to the result, and store the result in AX. The obvious solution is this:</P>
<!-- CODE SNIP //-->
<PRE>
mov ax,bx
add ax,di
add ax,2
</PRE>
<!-- END CODE SNIP //-->
<P>(It would be more compact to increment AX twice than to add two to it, and would probably be faster on an 8088, but that&#146;s not what we&#146;re after at the moment.) An elegant alternative solution is simply:
</P>
<!-- CODE SNIP //-->
<PRE>
lea ax,[bx&#43;di&#43;2]
</PRE>
<!-- END CODE SNIP //-->
<P>Likewise, either of the following would copy SI plus two to DI
</P>
<!-- CODE SNIP //-->
<PRE>
mov di,si
add di,2
</PRE>
<!-- END CODE SNIP //-->
<P>or:
</P>
<!-- CODE SNIP //-->
<PRE>
lea di,[si&#43;2]
</PRE>
<!-- END CODE SNIP //-->
<P>Mind you, the only components <B>LEA</B> can add are BX or BP, SI or DI, and a constant displacement, so it&#146;s not going to replace <B>ADD</B> most of the time. Also, <B>LEA</B> is considerably slower than <B>ADD</B> on an 8088, although it is just as fast as <B>ADD</B> on a 286 or 386 when fewer than three memory addressing components are used. <B>LEA</B> is 1 cycle slower than <B>ADD</B> on a 486 if the sum of two registers is used to point to memory, but no slower than <B>ADD</B> on a Pentium. On both a 486 and Pentium, <B>LEA</B> can also be slowed down by addressing interlocks.</P>
<P><A NAME="Fig1"><!-- </A><A HREF="javascript:displayWindow('images/06-01.jpg',418,276 )"> --><IMG SRC="images/06-01.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/06-01.jpg',418,276)"> --><FONT COLOR="#000077"><B>Figure 6.1</B></FONT></A>&nbsp;&nbsp;<I>Operation of ADD Reg,Reg vs. LEA Reg,{Addr}.</I>
</P>
<H4 ALIGN="LEFT"><A NAME="Heading5"></A><FONT COLOR="#000077">The Wonders of LEA on the 386</FONT></H4>
<P><B>LEA</B> really comes into its own as a &#147;super-ADD&#148; instruction on the 386, 486, and Pentium, where it can take advantage of the enhanced memory addressing modes of those processors. (The 486 and Pentium offer the same modes as the 386, so I&#146;ll refer only to the 386 from now on.) The 386 can do two very interesting things: It can use <I>any</I> 32-bit register (EAX, EBX, and so on) as the memory addressing base register and/or the memory addressing index register, and it can multiply any 32-bit register used as an index by two, four, or eight in the process of calculating a memory address, as shown in Figure 6.2. Let&#146;s see what that&#146;s good for.</P>
<P>Well, the obvious advantage is that any two 32-bit registers, or any 32-bit register and any constant, or any two 32-bit registers and any constant, can be added together, with the result stored in any register. This makes the 32-bit <B>LEA</B> much more generally useful than the standard 16-bit <B>LEA</B> in the role of an <B>ADD</B> with an independent destination.</P>
<P><A NAME="Fig2"><!-- </A><A HREF="javascript:displayWindow('images/06-02.jpg',421,197 )"> --><IMG SRC="images/06-02.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/06-02.jpg',421,197)"> --><FONT COLOR="#000077"><B>Figure 6.2</B></FONT></A>&nbsp;&nbsp;<I>Operation of the 32-bit LEA reg,[Addr].</I>
</P>
<P>But what else can <B>LEA</B> do on a 386, besides add?</P>
<P>It can multiply any register used as an index. <B>LEA</B> can multiply only by the power-of-two values 2, 4, or 8, but that&#146;s useful more often than you might imagine, especially when dealing with pointers into tables. Besides, multiplying by 2, 4, or 8 amounts to a left shift of 1, 2, or 3 bits, so we can now add up to two 32-bit registers and a constant, <I>and</I> shift (or multiply) one of the registers to some extent&#151;all with a single instruction. For example,</P>
<!-- CODE SNIP //-->
<PRE>
lea edi,TableBase[ecx&#43;edx*4]
</PRE>
<!-- END CODE SNIP //-->
<P>replaces all this
</P>
<!-- CODE SNIP //-->
<PRE>
mov edi,edx
shl edi,2
add edi,ecx
add edi,offset TableBase
</PRE>
<!-- END CODE SNIP //-->
<P>when pointing to an entry in a doubly indexed table.
</P>
<H3><A NAME="Heading6"></A><FONT COLOR="#000077">Multiplication with LEA Using Non-Powers of Two</FONT></H3>
<P>Are you impressed yet with all that <B>LEA</B> can do on the 386? Believe it or not, one more feature still awaits us. <B>LEA</B> can actually perform a fast multiply of a 32-bit register by some values <I>other</I> than powers of two. You see, the same 32-bit register can be both base and index on the 386, and can be scaled as the index while being used unchanged as the base. That means that you can, for example, multiply EBX by 5 with:</P>
<!-- CODE SNIP //-->
<PRE>
lea ebx,[ebx&#43;ebx*4]
</PRE>
<!-- END CODE SNIP //-->
<P>Without <B>LEA</B> and scaling, multiplication of EBX by 5 would require either a relatively slow <B>MUL</B>, along with a set-up instruction or two, or three separate instructions along the lines of the following</P>
<!-- CODE SNIP //-->
<PRE>
mov edx,ebx
shl ebx,2
add ebx,edx
</PRE>
<!-- END CODE SNIP //-->
<P>and would in either case require the destruction of the contents of another register.
</P>
<P>Multiplying a 32-bit value by a non-power-of-two multiplier in just 2 cycles is a pretty neat trick, even though it works only on a 386 or 486.</P>
<TABLE WIDTH="100%"><TD WIDTH="5%" VALIGN="TOP"><IMG SRC="images/06-01i.jpg"><TD WIDTH="95%"><SMALL><I>The full list of values that <B>LEA</B> can multiply a register by on a 386 or 486 is: 2, 3, 4, 5, 8, and 9. That list doesn&#146;t include every multiplier you might want, but it covers some commonly used ones, and the performance is hard to beat.</I></SMALL>
</TABLE>
<P>I&#146;d like to extend my thanks to Duane Strong of Metagraphics for his help in brainstorming uses for the 386 version of <B>LEA</B> and for pointing out the complications of 486 instruction timings.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="06-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch07/07-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

BIN
ch06/images/06-01.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
ch06/images/06-01i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch06/images/06-02.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

89
ch07/07-01.html Normal file
View file

@ -0,0 +1,89 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Local Optimization</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=07//-->
<!--PAGES=136-139//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch06/06-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="07-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H2><A NAME="Heading1"></A><FONT COLOR="#000077">Chapter 7<BR>Local Optimization
</FONT></H2>
<H3><A NAME="Heading2"></A><FONT COLOR="#000077">Optimizing Halfway between Algorithms and Cycle Counting</FONT></H3>
<P>You might not think it, but there&#146;s much to learn about performance programming from the Great Buffalo Sauna Fiasco. To wit:
</P>
<P>The scene is Buffalo, New York, in the dead of winter, with the snow piled several feet deep. Four college students, living in typical student housing, are frozen to the bone. The third floor of their house, uninsulated and so cold that it&#146;s uninhabitable, has an ancient bathroom. One fabulously cold day, inspiration strikes:</P>
<P>&#147;Hey&#151;we could make that bathroom into a <I>sauna!</I>&#148;</P>
<P>Pandemonium ensues. Someone rushes out and buys a gas heater, and at considerable risk to life and limb hooks it up to an abandoned but still live gas pipe that once fed a stove on the third floor. Someone else gets sheets of plastic and lines the walls of the bathroom to keep the moisture in, and yet another student gets a bucket full of rocks. The remaining chap brings up some old wooden chairs and sets them up to make benches along the sides of the bathroom. <I>Voila</I>&#151;instant sauna!</P>
<P>They crank up the gas heater, put the bucket of rocks in front of it, close the door, take off their clothes, and sit down to steam themselves. Mind you, it&#146;s not yet 50 degrees Fahrenheit in this room, but the gas heater is roaring. Surely warmer times await.</P>
<P>Indeed they do. The temperature climbs to 55 degrees, then 60, then 63, then 65, and finally creeps up to 68 degrees.</P>
<P>And there it stops.</P>
<P>68 degrees is warm for an uninsulated third floor in Buffalo in the dead of winter. Damn warm. It is not, however, particularly warm for a sauna. Eventually someone acknowledges the obvious and allows that it might have been a stupid idea after all, and everyone agrees, and they shut off the heater and leave, each no doubt offering silent thanks that they had gotten out of this without any incidents requiring major surgery.</P>
<P>And so we see that the best idea in the world can fail for lack of either proper design or adequate horsepower. The primary cause of the Great Buffalo Sauna Fiasco was a lack of horsepower; the gas heater was flat-out undersized. This is analogous to trying to write programs that incorporate features like bitmapped text and searching of multisegment buffers without using high-performance assembly language. Any PC language can perform just about any function you can think of&#151;eventually. That heater would eventually have heated the room to 110 degrees, too&#151;along about the first of June or so.</P>
<P>The Great Buffalo Sauna Fiasco also suffered from fundamental design flaws. A more powerful heater would indeed have made the room hotter&#151;and might well have burned the house down in the process. Likewise, proper algorithm selection and good design are fundamental to performance. The extra horsepower a superb assembly language implementation gives a program is worth bothering with only in the context of a good design.</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/07-01i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>Assembly language optimization is a small but crucial corner of the PC programming world. Use it sparingly and only within the framework of a good design&#151;but ignore it and you may find various portions of your anatomy out in the cold.</I></SMALL>
</TABLE>
<P>So, drawing fortitude from the knowledge that our quest is a pure and worthy one, let&#146;s resume our exploration of assembly language instructions with hidden talents and instructions with well-known talents that are less than they appear to be. In the process, we&#146;ll come to see that there is another, very important optimization level between the algorithm/design level and the cycle-counting/individual instruction level. I&#146;ll call this middle level <I>local optimization;</I> it involves focusing on optimizing sequences of instructions rather than individual instructions, all with an eye to implementing designs as efficiently as possible given the capabilities of the x86 family instruction set.</P>
<P>And yes, in case you&#146;re wondering, the above story is indeed true. Was I there? Let me put it this way: If I were, I&#146;d never admit it!</P>
<H4 ALIGN="LEFT"><A NAME="Heading3"></A><FONT COLOR="#000077">When LOOP Is a Bad Idea</FONT></H4>
<P>Let&#146;s examine first an instruction that is less than it appears to be: <B>LOOP</B>. There&#146;s no mystery about what <B>LOOP</B> does; it decrements CX and branches if CX doesn&#146;t decrement to zero. It&#146;s so beautifully suited to the task of counting down loops that any experienced x86 programmer instinctively stuffs the loop count in CX and reaches for <B>LOOP</B> when setting up a loop. That&#146;s fine&#151;<B>LOOP</B> does, of course, work as advertised&#151;but there is one problem:</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/07-02i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>On half of the processors in the x86 family, <B>LOOP</B> is slower than <B>DEC CX</B> followed by <B>JNZ</B>. (Granted, <B>DEC CX/JNZ</B> isn&#146;t precisely equivalent to <B>LOOP,</B> because <B>DEC</B> alters the flags and LOOP doesn&#146;t, but in most situations they&#146;re comparable.)</I></SMALL>
</TABLE>
<P>How can this be? Don&#146;t ask me, ask Intel. On the 8088 and 80286, <B>LOOP</B> is indeed faster than <B>DEC CX/JNZ</B> by a cycle, and <B>LOOP</B> is generally a little faster still because it&#146;s a byte shorter and so can be fetched faster. On the 386, however, things change; <B>LOOP</B> is two cycles <I>slower</I> than <B>DEC/JNZ,</B> and the fetch time for one extra byte on even an uncached 386 generally isn&#146;t significant. (Remember that the 386 fetches four instruction bytes at a pop.) <B>LOOP</B> is three cycles slower than <B>DEC/JNZ</B> on the 486, and the 486 executes instructions in so few cycles that those three cycles mean that <B>DEC/JNZ</B> is nearly <I>twice</I> as fast as <B>LOOP</B>. Then, too, unlike <B>LOOP, DEC</B> doesn&#146;t require that <B>CX</B> be used, so the <B>DEC/JNZ</B> solution is both faster and more flexible on the 386 and 486, and on the Pentium as well. (By the way, all this is not just theory; I&#146;ve timed the relative performances of <B>LOOP</B> and <B>DEC CX/JNZ</B> on a cached 386, and LOOP really is slower.)</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/07-03i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>Things are stranger still for <B>LOOP</B>&#146;s relative <B>JCXZ,</B> which branches if and only if CX is zero. <B>JCXZ</B> is faster than <B>AND CX,CX/JZ</B> on the 8088 and 80286, and equivalent on the 80386&#151;but is about twice as slow on the 486!</I></SMALL>
</TABLE>
<P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch06/06-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="07-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

91
ch07/07-02.html Normal file
View file

@ -0,0 +1,91 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Local Optimization</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=07//-->
<!--PAGES=139-141//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="07-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="07-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>By the way, don&#146;t fall victim to the lures of <B>JCXZ</B> and do something like this:</P>
<!-- CODE SNIP //-->
<PRE>
and cx,ofh ;Isolate the desired field
jcxz SkipLoop ;If field is 0, don&#146;t bother
</PRE>
<!-- END CODE SNIP //-->
<P>The <B>AND</B> instruction has already set the Zero flag, so this</P>
<!-- CODE SNIP //-->
<PRE>
and cx,0fh ;Isolate the desired field
jz SkipLoop ;If field is 0, don&#146;t bother
</PRE>
<!-- END CODE SNIP //-->
<P>will do just fine and is faster on all processors. Use <B>JCXZ</B> only when the Zero flag isn&#146;t already set to reflect the status of CX.</P>
<H3><A NAME="Heading4"></A><FONT COLOR="#000077">The Lessons of LOOP and JCXZ</FONT></H3>
<P>What can we learn from <B>LOOP</B> and <B>JCXZ?</B> First, that a single instruction that is intended to do a complex task is not necessarily faster than several instructions that together do the same thing. Second, that the relative merits of instructions and optimization rules vary to a surprisingly large degree across the x86 family.</P>
<P>In particular, if you&#146;re going to write 386 protected mode code, which will run only on the 386, 486, and Pentium, you&#146;d be well advised to rethink your use of the more esoteric members of the x86 instruction set. <B>LOOP, JCXZ,</B> the various accumulator-specific instructions, and even the string instructions in many circumstances no longer offer the advantages they did on the 8088. Sometimes they&#146;re just not any faster than more general instructions, so they&#146;re not worth going out of your way to use; sometimes, as with <B>LOOP,</B> they&#146;re actually slower, and you&#146;d do well to avoid them altogether in the 386/486 world. Reviewing the instruction cycle times in the MASM or TASM manuals, or looking over the cycle times in Intel&#146;s literature, is a good place to start; published cycle times are closer to actual execution times on the 386 and 486 than on the 8088, and are reasonably reliable indicators of the relative performance levels of x86 instructions.</P>
<H4 ALIGN="LEFT"><A NAME="Heading5"></A><FONT COLOR="#000077">Avoiding LOOPS of Any Stripe</FONT></H4>
<P>Cycle counting and directly substituting instructions (<B>DEC CX/JNZ</B> for <B>LOOP,</B> for example) are techniques that belong at the lowest level of optimization. It&#146;s an important level, but it&#146;s fairly mechanical; once you&#146;ve learned the capabilities and relative performance levels of the various instructions, you should be able to select the best instructions fairly easily. What&#146;s more, this is a task at which compilers excel. What I&#146;m saying is that you shouldn&#146;t get too caught up in counting cycles because that&#146;s a small (albeit important) part of the optimization picture, and not the area in which your greatest advantage lies.</P>
<H3><A NAME="Heading6"></A><FONT COLOR="#000077">Local Optimization</FONT></H3>
<P>One level at which assembly language programming pays off handsomely is that of <I>local optimization;</I> that is, selecting the best <I>sequence</I> of instructions for a task. The key to local optimization is viewing the 80x86 instruction set as a set of building blocks, each with unique characteristics. Your job is to sequence those blocks so that they perform well. It doesn&#146;t matter what the instructions are intended to do or what their names are; all that matters is what they <I>do.</I></P>
<P>Our discussion of <B>LOOP</B> versus <B>DEC/JNZ</B> is an excellent example of optimization by cycle counting. It&#146;s worth knowing, but once you&#146;ve learned it, you just routinely use <B>DEC/JNZ</B> at the bottom of loops in 386/486-specific code, and that&#146;s that. Besides, you&#146;ll save at most a few cycles each time, and while that helps a little, it&#146;s not going to make all <I>that</I> much difference.</P>
<P>Now let&#146;s step back for a moment, and with no preconceptions consider what the x86 instruction set can do for us. The bulk of the time with both <B>LOOP</B> and <B>DEC/JNZ</B> is taken up by branching, which just happens to be one of the slowest aspects of every processor in the x86 family, and the rest is taken up by decrementing the count register and checking whether it&#146;s zero. There may be ways to perform those tasks a little faster by selecting different instructions, but they can get only so fast, and branching can&#146;t even get all that fast.</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/07-04i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>The trick, then, is not to find the fastest way to decrement a count and branch conditionally, but rather to figure out how to accomplish the same result without decrementing or branching as often. Remember the Kobiyashi Maru problem in</I> Star Trek<I>?The same principle applies here: Redefine the problem to one that offers better solutions.</I></SMALL>
</TABLE>
<P>Consider Listing 7.1, which searches a buffer until either the specified byte is found, a zero byte is found, or the specified number of characters have been checked. Such a function would be useful for scanning up to a maximum number of characters in a zero-terminated buffer. Listing 7.1, which uses <B>LOOP</B> in the main loop, performs a search of the sample string for a period (&#145;.&#146;) in 170 &#181;s on a 20 MHz cached 386.</P>
<P>When the <B>LOOP</B> in Listing 7.1 is replaced with <B>DEC CX/JNZ,</B> performance improves to 168 &#181;s, less than 2 percent faster than Listing 7.1. Actually, instruction fetching, instruction alignment, cache characteristics, or something similar is affecting these results; I&#146;d expect a slightly larger improvement&#151;around 7 percent&#151;but that&#146;s the most that counting cycles could buy us in this case. (All right, already; <B>LOOPNZ</B> could be used at the bottom of the loop, and other optimizations are surely possible, but all that won&#146;t add up to anywhere near the benefits we&#146;re about to see from local optimization, and that&#146;s the whole point.)</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="07-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="07-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

160
ch07/07-03.html Normal file
View file

@ -0,0 +1,160 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Local Optimization</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=07//-->
<!--PAGES=141-143//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="07-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="07-04.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P><B>LISTING 7.1 L7-1.ASM</B></P>
<!-- CODE //-->
<PRE>
; Program to illustrate searching through a buffer of a specified
; length until either a specified byte or a zero byte is
; encountered.
; A standard loop terminated with LOOP is used.
.model small
.stack 100h
.data
; Sample string to search through.
SampleString labelbyte
db &#145;This is a sample string of a long enough length &#146;
db &#145;so that raw searching speed can outweigh any &#146;
db &#145;extra set-up time that may be required.&#146;,0
SAMPLE_STRING_LENGTH equ $-SampleString
; User prompt.
Prompt db &#145;Enter character to search for:$&#146;
; Result status messages.
ByteFoundMsg db 0dh,0ah
db &#145;Specified byte found.&#146;,0dh,0ah,&#145;$&#146;
ZeroByteFoundMsg db 0dh, 0ah
db &#145;Zero byte encountered.&#146;,0dh,0ah,&#145;$&#146;
NoByteFoundMsg db 0dh,0ah
db &#145;Buffer exhausted with no match.&#146;, 0dh, 0ah, &#145;$&#146;
.code
Startprocnear
mov ax,@data ;point to standard data segment
mov ds,ax
mov dx,offset Prompt
mov ah,9 ;DOS print string function
int 21h ;prompt the user
mov ah,1 ;DOS get key function
int 21h ;get the key to search for
mov ah,al ;put character to search for in AH
mov cx,SAMPLE_STRING_LENGTH ;# of bytes to search
mov si,offset SampleString ;point to buffer to search
call SearchMaxLength ;search the buffer
mov dx,offset ByteFoundMsg ;assume we found the byte
jc PrintStatus ;we did find the byte
;we didn&#146;t find the byte, figure out
;whether we found a zero byte or
;ran out of buffer
mov dx,offset NoByteFoundMsg
;assume we didn&#146;t find a zero byte
jcxz PrintStatus ;we didn&#146;t find a zero byte
mov dx,offset ZeroByteFoundMsg ;we found a zero byte
PrintStatus:
mov ah,9 ;DOS print string function
int 21h ;report status
mov ah,4ch ;return to DOS
int 21h
Startendp
; Function to search a buffer of a specified length until either a
; specified byte or a zero byte is encountered.
; Input:
; AH = character to search for
; CX = maximum length to be searched (must be &gt 0)
; DS:SI = pointer to buffer to be searched
; Output:
; CX = 0 if and only if we ran out of bytes without finding
; either the desired byte or a zero byte
; DS:SI = pointer to searched-for byte if found, otherwise byte
; after zero byte if found, otherwise byte after last
; byte checked if neither searched-for byte nor zero
; byte is found
; Carry Flag = set if searched-for byte found, reset otherwise
SearchMaxLengthprocnear
cld
SearchMaxLengthLoop:
lodsb ;get the next byte
cmp al,ah ;is this the byte we want?
jz ByteFound ;yes, we&#146;re done with success
and al,al ;is this the terminating 0 byte?
jz ByteNotFound ;yes, we&#146;re done with failure
loop SearchMaxLengthLoop ;it&#146;s neither, so check the next
;byte, if any
ByteNotFound:
clc ;return &#147;not found&#148; status
ret
ByteFound:
dec si ;point back to the location at which
;we found the searched-for byte
stc ;return &#147;found&#148; status
ret
SearchMaxLengthendp
end Start
</PRE>
<!-- END CODE //-->
<H3><A NAME="Heading7"></A><FONT COLOR="#000077">Unrolling Loops</FONT></H3>
<P>Listing 7.2 takes a different tack, unrolling the loop so that four bytes are checked for each <B>LOOP</B> performed. The same instructions are used inside the loop in each listing, but Listing 7.2 is arranged so that three-quarters of the <B>LOOP</B>s are eliminated. Listings 7.1 and 7.2 perform exactly the same task, and they use the same instructions in the loop&#151;the searching algorithm hasn&#146;t changed in any way&#151;but we have sequenced the instructions differently in Listing 7.2, and that makes all the difference.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="07-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="07-04.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

200
ch07/07-04.html Normal file
View file

@ -0,0 +1,200 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Local Optimization</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=07//-->
<!--PAGES=143-145//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="07-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="07-05.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P><B>LISTING 7.2 L7-2.ASM</B></P>
<!-- CODE //-->
<PRE>
; Program to illustrate searching through a buffer of a specified
; length until a specified zero byte is encountered.
; A loop unrolled four times and terminated with LOOP is used.
.model small
.stack 100h
.data
; Sample string to search through.
SampleStringlabelbyte
db &#145;This is a sample string of a long enough length &#146;
db &#145;so that raw searching speed can outweigh any &#146;
db &#145;extra set-up time that may be required.&#146;,0
SAMPLE_STRING_LENGTH equ $-SampleString
; User prompt.
Prompt db &#145;Enter character to search for:$&#146;
; Result status messages.
ByteFoundMsg db 0dh,0ah
db &#145;Specified byte found.&#146;,0dh,0ah,&#145;$&#146;
ZeroByteFoundMsg db 0dh,0ah
db &#145;Zero byte encountered.&#146;, 0dh, 0ah, &#145;$&#146;
NoByteFoundMsg db 0dh,0ah
db &#145;Buffer exhausted with no match.&#146;, 0dh, 0ah, &#145;$&#146;
; Table of initial, possibly partial loop entry points for
; SearchMaxLength.
SearchMaxLengthEntryTable labelword
dw SearchMaxLengthEntry4
dw SearchMaxLengthEntry1
dw SearchMaxLengthEntry2
dw SearchMaxLengthEntry3
.code
Start proc near
mov ax,@data ;point to standard data segment
mov ds,ax
mov dx,offset Prompt
mov ah,9 ;DOS print string function
int 21h ;prompt the user
mov ah,1 ;DOS get key function
int 21h ;get the key to search for
mov ah,al ;put character to search for in AH
mov cx,SAMPLE_STRING_LENGTH ;# of bytes to search
mov si,offset SampleString ;point to buffer to search
call SearchMaxLength ;search the buffer
mov dx,offset ByteFoundMsg ;assume we found the byte
jc PrintStatus ;we did find the byte
;we didn&#146;t find the byte, figure out
;whether we found a zero byte or
;ran out of buffer
mov dx,offset NoByteFoundMsg
;assume we didn&#146;t find a zero byte
jcxz PrintStatus ;we didn&#146;t find a zero byte
mov dx,offset ZeroByteFoundMsg ;we found a zero byte
PrintStatus:
mov ah,9 ;DOS print string function
int 21h ;report status
mov ah,4ch ;return to DOS
int 21h
Startendp
; Function to search a buffer of a specified length until either a
; specified byte or a zero byte is encountered.
; Input:
; AH = character to search for
; CX = maximum length to be searched (must be &gt 0)
; DS:SI = pointer to buffer to be searched
; Output:
; CX = 0 if and only if we ran out of bytes without finding
; either the desired byte or a zero byte
; DS:SI = pointer to searched-for byte if found, otherwise byte
; after zero byte if found, otherwise byte after last
; byte checked if neither searched-for byte nor zero
; byte is found
; Carry Flag = set if searched-for byte found, reset otherwise
SearchMaxLength proc near
cld
mov bx,cx
add cx,3 ;calculate the maximum # of passes
shr cx,1 ;through the loop, which is
shr cx,1 ;unrolled 4 times
and bx,3 ;calculate the index into the entry
;point table for the first,
;possibly partial loop
shl bx,1 ;prepare for a word-sized look-up
jmp SearchMaxLengthEntryTable[bx]
;branch into the unrolled loop to do
;the first, possibly partial loop
SearchMaxLengthLoop:
SearchMaxLengthEntry4:
lodsb ;get the next byte
cmp al,ah ;is this the byte we want?
jz ByteFound ;yes, we&#146;re done with success
and al,al ;is this the terminating 0 byte?
jz ByteNotFound ;yes, we&#146;re done with failure
SearchMaxLengthEntry3:
lodsb ;get the next byte
cmp al,ah ;is this the byte we want?
jz ByteFound ;yes, we&#146;re done with success
and al,al ;is this the terminating 0 byte?
jz ByteNotFound ;yes, we&#146;re done with failure
SearchMaxLengthEntry2:
lodsb ;get the next byte
cmp al,ah ;is this the byte we want?
jz ByteFound ;yes, we&#146;re done with success
and al,al ;is this the terminating 0 byte?
jz ByteNotFound ;yes, we&#146;re done with failure
SearchMaxLengthEntry1:
lodsb ;get the next byte
cmp al,ah ;is this the byte we want?
jz ByteFound ;yes, we&#146;re done with success
and al,al ;is this the terminating 0 byte?
jz ByteNotFound ;yes, we&#146;re done with failure
loop SearchMaxLengthLoop ;it&#146;s neither, so check the next
; four bytes, if any
ByteNotFound:
clc ;return &#147;not found&#148; status
ret
ByteFound:
dec si ;point back to the location at which
; we found the searched-for byte
stc ;return &#147;found&#148; status
ret
SearchMaxLengthendp
end Start
</PRE>
<!-- END CODE //-->
<P>How much difference? Listing 7.2 runs in 121 &#181;s&#151;40 percent faster than Listing 7.1, even though Listing 7.2 still uses <B>LOOP</B> rather than <B>DEC CX/JNZ.</B> (The loop in Listing 7.2 could be unrolled further, too; it&#146;s just a question of how much more memory you want to trade for ever-decreasing performance benefits.) That&#146;s typical of local optimization; it won&#146;t often yield the order-of-magnitude improvements that algorithmic improvements can produce, but it can get you a critical 50 percent or 100 percent improvement when you&#146;ve exhausted all other avenues.</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/07-05i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>The point is simply this: You can gain far more by stepping back a bit and thinking of the fastest overall way for the CPU to perform a task than you can by saving a cycle here or there using different instructions. Try to think at the level of sequences of instructions rather than individual instructions, and learn to treat x86 instructions as building blocks with unique characteristics rather than as instructions dedicated to specific tasks.</I></SMALL>
</TABLE>
<P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="07-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="07-05.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

168
ch07/07-05.html Normal file
View file

@ -0,0 +1,168 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Local Optimization</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=07//-->
<!--PAGES=145-148//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="07-04.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch08/08-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H4 ALIGN="LEFT"><A NAME="Heading8"></A><FONT COLOR="#000077">Rotating and Shifting with Tables</FONT></H4>
<P>As another example of local optimization, consider the matter of rotating or shifting a mask into position. First, let&#146;s look at the simple task of setting bit N of AX to 1.
</P>
<P>The obvious way to do this is to place N in CL, rotate the bit into position, and OR it with AX, as follows:</P>
<!-- CODE SNIP //-->
<PRE>
MOV BX,1
SHL BX,CL
OR AX,BX
</PRE>
<!-- END CODE SNIP //-->
<P>This solution is obvious because it takes good advantage of the special ability of the x86 family to shift or rotate by the variable number of bits specified by CL. However, it takes an average of about 45 cycles on an 8088. It&#146;s actually far faster to precalculate the results, pass the bit number in BX, and look the shifted bit up, as shown in Listing 7.3.
</P>
<P><B>LISTING 7.3 L7-3.ASM</B></P>
<!-- CODE //-->
<PRE>
SHL BX,1 ;prepare for word sized look up
OR AX,ShiftTable[BX] ;look up the bit and OR it in
:
ShiftTable LABEL WORD
BIT_PATTERN=0001H
REPT 16
DW BIT_PATTERN
BIT_PATTERN=BIT_PATTERN SHL 1
ENDM
</PRE>
<!-- END CODE //-->
<P>Even though it accesses memory, this approach takes only 20 cycles&#151;more than twice as fast as the variable shift. Once again, we were able to improve performance considerably&#151;not by knowing the fastest instructions, but by selecting the fastest <I>sequence</I> of instructions.</P>
<P>In the particular example above, we once again run into the difficulty of optimizing across the x86 family. The table lookup is faster on the 8088 and 286, but it&#146;s slightly slower on the 386 and no faster on the 486. However, 386/486-specific code could use enhanced addressing to accomplish the whole job in just one instruction, along the lines of the code snippet in Listing 7.4.</P>
<P><B>LISTING 7.4 L7-4.ASM</B></P>
<!-- CODE SNIP //-->
<PRE>
OR EAX,ShiftTable[EBX*4] ;look up the bit and OR it in
:
ShiftTable LABEL DWORD
BIT_PATTERN=0001H
REPT 32
DD BIT_PATTERN
BIT_PATTERN=BIT_PATTERN SHL 1
ENDM
</PRE>
<!-- END CODE SNIP //-->
<TABLE WIDTH="100%">
<TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/07-06i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>Besides illustrating the advantages of local optimization, this example also shows that it generally pays to precalculate results; this is often done at or before assembly time, but precalculated tables can also be built at run time. This is merely one aspect of a fundamental optimization rule: Move as much work as possible out of your critical code by whatever means necessary.</I></SMALL>
</TABLE>
<H4 ALIGN="LEFT"><A NAME="Heading9"></A><FONT COLOR="#000077">NOT Flips Bits&#151;Not Flags</FONT></H4>
<P>The <B>NOT</B> instruction flips all the bits in the operand, from 0 to 1 or from 1 to 0. That&#146;s as simple as could be, but <B>NOT</B> nonetheless has a minor but interesting talent: It doesn&#146;t affect the flags. That can be irritating; I once spent a good hour tracking down a bug caused by my unconscious assumption that <B>NOT</B> does set the flags. After all, every other arithmetic and logical instruction sets the flags; why not <B>NOT</B>? Probably because <B>NOT</B> isn&#146;t considered to be an arithmetic or logical instruction at all; rather, it&#146;s a data manipulation instruction, like <B>MOV</B> and the various rotates. (These are <B>RCR, RCL, ROR,</B> and <B>ROL,</B> which affect only the Carry and Overflow flags.) NOT is often used for tasks, such as flipping masks, where there&#146;s no reason to test the state of the result, and in that context it can be handy to keep the flags unmodified for later testing.</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/07-07i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>Besides, if you want to <B>NOT</B> an operand and set the flags in the process, you can just <B>XOR</B> it with -1. Put another way, the only functional difference between <B>NOT AX</B> and <B>XOR AX,0FFFFH</B> is that <B>XOR</B> modifies the flags and <B>NOT</B> doesn&#146;t.</I></SMALL>
</TABLE>
<P>The x86 instruction set offers many ways to accomplish almost any task. Understanding the subtle distinctions between the instructions&#151;whether and which flags are set, for example&#151;can be critical when you&#146;re trying to optimize a code sequence and you&#146;re running out of registers, or when you&#146;re trying to minimize branching.
</P>
<H4 ALIGN="LEFT"><A NAME="Heading10"></A><FONT COLOR="#000077">Incrementing with and without Carry</FONT></H4>
<P>Another case in which there are two slightly different ways to perform a task involves adding 1 to an operand. You can do this with <B>INC,</B> as in <B>INC AX,</B> or you can do it with <B>ADD,</B> as in <B>ADD AX,1.</B> What&#146;s the difference? The obvious difference is that <B>INC</B> is usually a byte or two shorter (the exception being <B>ADD AL,1,</B> which at two bytes is the same length as <B>INC AL</B>), and is faster on some processors. Less obvious, but no less important, is that <B>ADD</B> sets the Carry flag while <B>INC</B> leaves the Carry flag untouched.</P>
<P>Why is that important? Because it allows <B>INC</B> to function as a data pointer manipulation instruction for multi-word arithmetic. You can use <B>INC</B> to advance the pointers in code like that shown in Listing 7.5 without having to do any work to preserve the Carry status from one addition to the next.</P>
<P><B>LISTING 7.5 L7-5.ASM</B></P>
<!-- CODE //-->
<PRE>
CLC ;clear the Carry for the initial addition
LOOP_TOP:
MOV AX,[SI];get next source operand word
ADC [DI],AX;add with Carry to dest operand word
INC SI ;point to next source operand word
INC SI
INC DI ;point to next dest operand word
INC DI
LOOP LOOP_TOP
</PRE>
<!-- END CODE //-->
<P>If <B>ADD</B> were used, the Carry flag would have to be saved between additions, with code along the lines shown in Listing 7.6.</P>
<P><B>LISTING 7.6 L7-6.ASM</B></P>
<!-- CODE //-->
<PRE>
CLC ;clear the carry for the initial addition
LOOP_TOP:
MOV AX,[SI] ;get next source operand word
ADC [DI],AX ;add with carry to dest operand word
LAHF ;set aside the carry flag
ADD SI,2 ;point to next source operand word
ADD DI,2 ;point to next dest operand word
SAHF ;restore the carry flag
LOOP LOOP_TOP
</PRE>
<!-- END CODE //-->
<P>It&#146;s not that the Listing 7.6 approach is necessarily better or worse; that depends on the processor and the situation. The Listing 7.6 approach is <I>different,</I> and if you understand the differences, you&#146;ll be able to choose the best approach for whatever code you happen to write. (<B>DEC</B> has the same property of preserving the Carry flag, by the way.)</P>
<P>There are a couple of interesting aspects to the last example. First, note that <B>LOOP</B> doesn&#146;t affect any flags at all; this allows the Carry flag to remain unchanged from one addition to the next. Not altering the arithmetic flags is a common characteristic of program control instructions (as opposed to arithmetic and logical instructions like <B>SUB</B> and <B>AND,</B> which do alter the flags).</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/07-08i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>The rule is not that the arithmetic flags change whenever the CPU performs a calculation; rather, the flags change whenever you execute an arithmetic, logical, or flag control (such as <B>CLC</B> to clear the Carry flag) instruction.</I></SMALL>
</TABLE>
<P>Not only do <B>LOOP</B> and <B>JCXZ</B> not alter the flags, but <B>REP MOVS</B>, which counts down CX to 0, doesn&#146;t affect the flags either.</P>
<P>The other interesting point about the last example is the use of <B>LAHF</B> and <B>SAHF,</B> which transfer the low byte of the FLAGS register to and from AH, respectively. These instructions were created to help provide compatibility with the 8080&#146;s (that&#146;s <I>8080</I>, not <I>8088</I>) <B>PUSH</B> <B>PSW</B> and <B>POP PSW</B> instructions, but turn out to be compact (one byte) instructions for saving and restoring the arithmetic flags. A word of caution, however: <B>SAHF</B> restores the Carry, Zero, Sign, Auxiliary Carry, and Parity flags&#151;but <I>not</I> the Overflow flag, which resides in the high byte of the FLAGS register. Also, be aware that <B>LAHF</B> and <B>SAHF</B> provide a fast way to preserve the flags on an 8088 but are relatively slow instructions on the 486 and Pentium.</P>
<P>There are times when it&#146;s a clear liability that <B>INC</B> doesn&#146;t set the Carry flag. For instance</P>
<!-- CODE SNIP //-->
<PRE>
INC AX
ADC DX,0
</PRE>
<!-- END CODE SNIP //-->
<P>does <I>not</I> increment the 32-bit value in DX:AX. To do that, you&#146;d need the following:</P>
<!-- CODE SNIP //-->
<PRE>
ADD AX,1
ADC DX,0
</PRE>
<!-- END CODE SNIP //-->
<P>As always, pay attention!
</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="07-04.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch08/08-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

BIN
ch07/images/07-01i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch07/images/07-02i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch07/images/07-03i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch07/images/07-04i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch07/images/07-05i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch07/images/07-06i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch07/images/07-07i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch07/images/07-08i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

86
ch08/08-01.html Normal file
View file

@ -0,0 +1,86 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Speeding Up C with Assembly Language</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=08//-->
<!--PAGES=149-153//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch07/07-05.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="08-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H2><A NAME="Heading1"></A><FONT COLOR="#000077">Chapter 8<BR>Speeding Up C with Assembly Language
</FONT></H2>
<H3><A NAME="Heading2"></A><FONT COLOR="#000077">Jumping Languages When You Know It&#146;ll Help</FONT></H3>
<P>When I was a senior in high school, a pop song called &#147;Seasons in the Sun,&#148; sung by one Terry Jacks, soared up the pop charts and spent, as best I can recall, two straight weeks atop <I>Kasey Kasem&#146;s American Top 40.</I> &#147;Seasons in the Sun&#148; wasn&#146;t a particularly good song, primarily because the lyrics were silly. I&#146;ve never understood why the song was a hit, but, as so often happens with undistinguished but popular music by forgotten one- or two-shot groups (&#147;Don&#146;t Pull Your Love Out on Me Baby,&#148; &#147;Billy Don&#146;t Be a Hero,&#148; <I>et al.</I>), I heard it everywhere for a month or so, then gave it not another thought for 15 years.</P>
<P>Recently, though, I came across a review of a Rhino Records collection of obscure 1970s pop hits. Knowing that Jeff Duntemann is an aficionado of such esoterica (who do <I>you</I> know who owns an album by The Peppermint Trolley Company?), I sent the review to him. He was amused by it and, as we kicked the names of old songs around, &#147;Seasons in the Sun&#148; came up. I expressed my wonderment that a song that really wasn&#146;t very good was such a big hit.</P>
<P>&#147;Well,&#148; said Jeff, &#147;I think it suffered in the translation from the French.&#148;</P>
<P>Ah-ha! Mystery solved. Apparently everyone but me knew that it was translated from French, and that novelty undoubtedly made the song a big hit. The translation was also surely responsible for the sappy lyrics; dollars to donuts that the original French lyrics were stronger.</P>
<P>Which brings us without missing a beat to this chapter&#146;s theme, speeding up C with assembly language. When you seek to speed up a C program by converting selected parts of it (generally no more than a few functions) to assembly language, make sure you end up with high-performance assembly language code, not fine-tuned C code. Compilers like Microsoft C/C<SMALL>&#43;&#43;</SMALL> and Watcom C are by now pretty good at fine-tuning C code, and you&#146;re not likely to do much better by taking the compiler&#146;s assembly language output and tweaking it.</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/08-01i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>To make the process of translating C code to assembly language worth the trouble, you must ignore what the compiler does and design your assembly language code from a pure assembly language perspective. With a merely adequate translation, you risk laboring mightily for little or no reward.</I></SMALL>
</TABLE>
<P>Apropos of which, when was the last time you heard of Terry Jacks?
</P>
<H4 ALIGN="LEFT"><A NAME="Heading3"></A><FONT COLOR="#000077">Billy, Don&#146;t Be a Compiler</FONT></H4>
<P>The key to optimizing C programs with assembly language is, as always, writing good assembly language code, but with an added twist. Rule 1 when converting C code to assembly is this: <I>Don&#146;t think like a compiler.</I> That&#146;s more easily said than done, especially when the C code you&#146;re converting is readily available as a model and the assembly code that the compiler generates is available as well. Nevertheless, the principle of not thinking like a compiler is essential, and is, in one form or another, the basis for all that I&#146;ll discuss below.</P>
<P>Before I discuss Rule 1 further, let me mention rule number 0: <I>Only optimize where it matters.</I> The bulk of execution time in any program is spent in a very small portion of the code, and most code beyond that small portion doesn&#146;t have any perceptible impact on performance. Unless you&#146;re supremely concerned with code size (an area in which assembly-only programs can excel), I&#146;d suggest that you write most of your code in C and reserve assembly for the truly critical sections of your code; that&#146;s the formula that I find gives the most bang for the buck.</P>
<P>This is not to say that complete programs shouldn&#146;t be <I>designed</I> with optimized assembly language in mind. As you&#146;ll see shortly, orienting your data structures towards assembly language can be a salubrious endeavor indeed, even if most of your code is in C. When it comes to actually optimizing code and/or converting it to assembly, though, do it only where it matters. Get a profiler&#151;and use it!</P>
<P>Also make it a point to concentrate on refining your program design and algorithmic approach at the conceptual and/or C levels before doing any assembly language optimization.</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/08-02i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>Assembly language optimization is the final and far from the only step in the optimization chain, and as such should be performed last; converting to assembly too soon can lock in your code before the design is optimal. At the very least, conversion to assembly tends to make future changes and debugging more difficult, slowing you down and limiting your options.</I></SMALL>
</TABLE>
<H3><A NAME="Heading4"></A><FONT COLOR="#000077">Don&#146;t Call Your Functions on Me, Baby</FONT></H3>
<P>In order to think differently from a compiler, you must understand both what compilers and C programmers tend to do and how that differs from what assembly language does well. In this pursuit, it can be useful to examine the code your compiler generates, either by viewing the code in a debugger or by having the compiler generate an assembly language output file. (The latter is done with /Fa or /Fc in Microsoft C/C<SMALL>&#43;&#43;</SMALL> and -S in Borland C<SMALL>&#43;&#43;</SMALL>.)</P>
<P>C programmers tend to modularize their code with lots of function calls. That&#146;s good for readable, reliable, reusable code, and it allows the compiler to optimize better because it can deal with fewer variables and statements in each optimization arena&#151;but it&#146;s not so good when viewed from the assembly language level. Calls and returns are slow, especially in the large code model, and the pushes required to put parameters on the stack are expensive as well.</P>
<P>What this means is that when you want to speed up a portion of a C program, you should identify the entire critical portion and move <I>all</I> of that critical portion into an assembly language function. You don&#146;t want to move a part of the inner loop into assembly language and then call it from C every time through the loop; the function call and return overhead would be unacceptable. Carve out the critical code <I>en masse</I> and move it into assembly, and try to avoid calls and returns even in your assembly code. True, in assembly you can pass parameters in registers, but the calls and returns themselves are still slow; if the extra cycles they take don&#146;t affect performance, then the code they&#146;re in probably isn&#146;t critical, and perhaps you&#146;ve chosen to convert too much code to assembly, eh?</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="../ch07/07-05.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="08-02.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

115
ch08/08-02.html Normal file
View file

@ -0,0 +1,115 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Speeding Up C with Assembly Language</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=08//-->
<!--PAGES=153-156//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="08-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="08-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<H3><A NAME="Heading5"></A><FONT COLOR="#000077">Stack Frames Slow So Much</FONT></H3>
<P>C compilers work within the stack frame model, whereby variables reside in a block of stack memory and are accessed via offsets from BP. Compilers may store a couple of variables in registers and may briefly keep other variables in registers when they&#146;re used repeatedly, but the stack frame is the underlying architecture. It&#146;s a nice architecture; it&#146;s flexible, convenient, easy to program, and makes for fairly compact code. However, stack frames have a few drawbacks. They must be constructed and destroyed, which takes both time and code. They are so easy to use that they tend to bias the assembly language programmer in favor of accessing memory variables more often than might be necessary. Finally, you cannot use BP as a general-purpose register if you intend to access a stack frame, and having that seventh register available is sometimes useful indeed.
</P>
<P>That doesn&#146;t mean you shouldn&#146;t use stack frames, which are useful and often necessary. Just don&#146;t fall victim to their undeniable charms.</P>
<H3><A NAME="Heading6"></A><FONT COLOR="#000077">Torn Between Two Segments</FONT></H3>
<P>C compilers are not terrific at handling segments. Some compilers can efficiently handle a single far pointer used in a loop by leaving ES set for the duration of the loop. But two far pointers used in the same loop confuse every compiler I&#146;ve seen, causing the full segment:offset address to be reloaded each time either pointer is used.
</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/08-03i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>This particularly affects performance in 286 protected mode (under OS/2 1.X or the Rational DOS Extender, for example) because segment loads in protected mode take a minimum of 17 cycles, versus a mere 2 cycles in real mode. </I></SMALL>
</TABLE>
<P>In assembly language you have full control over segments. Use it, and, if necessary, reorganize your code to minimize segment loading.
</P>
<H4 ALIGN="LEFT"><A NAME="Heading7"></A><FONT COLOR="#000077">Why Speeding Up Is Hard to Do</FONT></H4>
<P>You might think that the most obvious advantage assembly language has over C is that it allows the use of all forms of instructions and all registers in all ways, whereas C compilers tend to use a subset of registers and instructions in a limited number of ways. Yes and no. It&#146;s true that C compilers typically don&#146;t generate instructions such as <B>XLAT,</B> rotates, or the string instructions. On the other hand, <B>XLAT</B> and rotates are useful in a limited set of circumstances, and string instructions <I>are</I> used in the C library functions. In fact, C library code is likely to be carefully optimized by experts, and may be much better than equivalent code you&#146;d produce yourself.</P>
<P>Am I saying that C compilers produce better code than you do? No, I&#146;m saying that they <I>can,</I> unless you use assembly language properly. Writing code in assembly language rather than C guarantees nothing.</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/08-04i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>You can write good assembly, bad assembly, or assembly that is virtually indistinguishable from compiled code; you are more likely than not to write the latter if you think that optimization consists of tweaking compiled C code. </I></SMALL>
</TABLE>
<P>Sure, you can probably use the registers more efficiently and take advantage of an instruction or two that the compiler missed, but the code isn&#146;t going to get a whole lot faster that way.
</P>
<P>True optimization requires rethinking your code to take advantage of assembly language. A C loop that searches through an integer array for matches might compile</P>
<P><A NAME="Fig1"><!-- </A><A HREF="javascript:displayWindow('images/08-01.jpg',413,198 )"> --><IMG SRC="images/08-01.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/08-01.jpg',413,198)"> --><FONT COLOR="#000077"><B>Figure 8.1</B></FONT></A>&nbsp;&nbsp;<I>Tweaked compiler output for a loop.</I>
</P>
<P>to something like Figure 8.1A. You might look at that and tweak it to the code shown in Figure 8.1B.
</P>
<P>Congratulations! You&#146;ve successfully eliminated all stack frame access, you&#146;ve used <B>LOOP</B> (although <B>DEC SI/JNZ</B> is actually faster on 386 and later machines, as I explained in the last chapter), and you&#146;ve used a string instruction. Unfortunately, the new code isn&#146;t going to run very much faster. Maybe 25 percent faster, maybe a little more. Big deal. You&#146;ve eliminated the trappings of the compiler&#151;the stack frame and the restricted register usage&#151;but you&#146;re still <I>thinking</I> like the compiler. Try this:</P>
<!-- CODE SNIP //-->
<PRE>
repnz scasw
jz Match
</PRE>
<!-- END CODE SNIP //-->
<P>It&#146;s a simple example&#151;but, I hope, a convincing one. Stretch your brain when you optimize.
</P>
<H3><A NAME="Heading8"></A><FONT COLOR="#000077">Taking It to the Limit</FONT></H3>
<P>The ultimate in assembly language optimization comes when you change the rules; that is, when you reorganize the entire program to allow the use of better assembly language code in the small section of code that most affects overall performance. For example, consider that the data searched in the last example is stored in an array of structures, with each structure in the array containing other information as well. In this situation, <B>REP SCASW</B> couldn&#146;t be used because the data searched through wouldn&#146;t be contiguous.</P>
<P>However, if the need for performance in searching the array is urgent enough, there&#146;s no reason why you can&#146;t reorganize the data. This might mean removing the array elements from the structures and storing them in their own array so that <B>REP SCASW</B> <I>could</I> be used.</P>
<TABLE WIDTH="100%"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="5%"><IMG SRC="images/08-05i.jpg"><TD ALIGN="LEFT" VALIGN="TOP" WIDTH="95%"><SMALL><I>Organizing a program&#146;s data so that the performance of the critical sections can be optimized is a key part of design, and one that&#146;s easily shortchanged unless, during the design stage, you thoroughly understand and work to bring together your data needs, the critical sections of your program, and potential assembly language optimizations.</I></SMALL>
</TABLE>
<P>More on this shortly.
</P>
<P>To recap, here are some things to look for when striving to convert C code into optimized assembly language:</P>
<DL>
<DD><B>&#149;</B>&nbsp;&nbsp;Move the entire performance-critical section into a single assembly language function.
<DD><B>&#149;</B>&nbsp;&nbsp;Don&#146;t use calls or stack frame accesses inside the critical code, if possible, and avoid unnecessary memory accesses of any kind.
<DD><B>&#149;</B>&nbsp;&nbsp;Change segments as infrequently as possible.
<DD><B>&#149;</B>&nbsp;&nbsp;Optimize in terms of what assembly does well, <I>not</I> in terms of fine-tuning compiled C code.
<DD><B>&#149;</B>&nbsp;&nbsp;Change the rules to the benefit of assembly, if necessary; for example, reorganize data structto allow efficient assembly language processing.
</DL>
<P>That said, let me show some of these precepts in action.
</P>
<H4 ALIGN="LEFT"><A NAME="Heading9"></A><FONT COLOR="#000077">A C-to-Assembly Case Study</FONT></H4>
<P>Listing 8.1 is the sample C application I&#146;m going to use to examine optimization in action. Listing 8.1 isn&#146;t really complete&#151;it doesn&#146;t handle the &#147;no-matches&#148; case well, and it assumes that the sum of all matches will fit into an <B>int&#151;</B>but it will do just fine as an optimization example.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="08-01.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="08-03.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

227
ch08/08-03.html Normal file
View file

@ -0,0 +1,227 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Speeding Up C with Assembly Language</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=08//-->
<!--PAGES=156-160//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="08-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="08-04.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P><B>LISTING 8.1 L8-1.C</B></P>
<!-- CODE //-->
<PRE>
/* Program to search an array spanning a linked list of variable-
sized blocks, for all entries with a specified ID number,
and return the average of the values of all such entries. Each of
the variable-sized blocks may contain any number of data entries,
stored as an array of structures within the block. */
#include &ltstdio.h&gt
#ifdef __TURBOC__
#include &ltalloc.h&gt
#else
#include &ltmalloc.h&gt
#endif
void main(void);
void exit(int);
unsigned int FindIDAverage(unsigned int, struct BlockHeader *);
/* Structure that starts each variable-sized block */
struct BlockHeader {
struct BlockHeader *NextBlock; /* Pointer to next block, or NULL
if this is the last block in the
linked list */
unsigned int BlockCount; /* The number of DataElement entries
in this variable-sized block */
};
/* Structure that contains one element of the array we&#146;ll search */
struct DataElement {
unsigned int ID; /* ID # for array entry */
unsigned int Value; /* Value of array entry */
};
void main(void) {
int i,j;
unsigned int IDToFind;
struct BlockHeader *BaseArrayBlockPointer,*WorkingBlockPointer;
struct DataElement *WorkingDataPointer;
struct BlockHeader **LastBlockPointer;
printf(&#148;ID # for which to find average: &#147;);
scanf(&#148;%d&#148;,&ampIDToFind);
/* Build an array across 5 blocks, for testing */
/* Anchor the linked list to BaseArrayBlockPointer */
LastBlockPointer = &ampBaseArrayBlockPointer;
/* Create 5 blocks of varying sizes */
for (i = 1; i &lt 6; i&#43;&#43;) {
/* Try to get memory for the next block */
if ((WorkingBlockPointer =
(struct BlockHeader *) malloc(sizeof(struct BlockHeader) &#43;
sizeof(struct DataElement) * i * 10)) == NULL) {
exit(1);
}
/* Set the # of data elements in this block */
WorkingBlockPointer-&gtBlockCount = i * 10;
/* Link the new block into the chain */
*LastBlockPointer = WorkingBlockPointer;
/* Point to the first data field */
WorkingDataPointer =
(struct DataElement *) ((char *)WorkingBlockPointer &#43;
sizeof(struct BlockHeader));
/* Fill the data fields with ID numbers and values */
for (j = 0; j &lt (i * 10); j&#43;&#43;, WorkingDataPointer&#43;&#43;) {
WorkingDataPointer-&gtID = j;
WorkingDataPointer-&gtValue = i * 1000 &#43; j;
}
/* Remember where to set link from this block to the next */
LastBlockPointer = &ampWorkingBlockPointer-&gtNextBlock;
}
/* Set the last block&#146;s &#147;next block&#148; pointer to NULL to indicate
that there are no more blocks */
WorkingBlockPointer-&gtNextBlock = NULL;
printf(&#148;Average of all elements with ID %d: %u\n&#148;,
IDToFind, FindIDAverage(IDToFind, BaseArrayBlockPointer));
exit(0);
}
/* Searches through the array of DataElement entries spanning the
linked list of variable-sized blocks, starting with the block
pointed to by BlockPointer, for all entries with IDs matching
SearchedForID, and returns the average value of those entries. If
no matches are found, zero is returned */
unsigned int FindIDAverage(unsigned int SearchedForID,
struct BlockHeader *BlockPointer)
{
struct DataElement *DataPointer;
unsigned int IDMatchSum;
unsigned int IDMatchCount;
unsigned int WorkingBlockCount;
IDMatchCount = IDMatchSum = 0;
/* Search through all the linked blocks until the last block
(marked with a NULL pointer to the next block) has been
searched */
do {
/* Point to the first DataElement entry within this block */
DataPointer =
(struct DataElement *) ((char *)BlockPointer &#43;
sizeof(struct BlockHeader));
/* Search all the DataElement entries within this block
and accumulate data from all that match the desired ID */
for (WorkingBlockCount=0;
WorkingBlockCount&ltBlockPointer-&gtBlockCount;
WorkingBlockCount&#43;&#43;, DataPointer&#43;&#43;) {
/* If the ID matches, add in the value and increment the
match counter */
if (DataPointer-&gtID == SearchedForID) {
IDMatchCount&#43;&#43;;
IDMatchSum &#43;= DataPointer-&gtValue;
}
}
/* Point to the next block, and continue as long as that pointer
isn&#146;t NULL */
} while ((BlockPointer = BlockPointer-&gtNextBlock) != NULL);
/* Calculate the average of all matches */
if (IDMatchCount == 0)
return(0); /* Avoid division by 0 */
else
return(IDMatchSum / IDMatchCount);
}
</PRE>
<!-- END CODE //-->
<P>The main body of Listing 8.1 constructs a linked list of memory blocks of various sizes and stores an array of structures across those blocks, as shown in Figure 8.2. The function <B>FindIDAverage</B> in Listing 8.1 searches through that array for all matches to a specified ID number and returns the average value of all such matches. <B>FindIDAverage</B> contains two nested loops, the outer one repeating once for each linked block and the inner one repeating once for each array element in each block. The inner loop&#151;the critical one&#151;is compact, containing only four statements, and should lend itself rather well to compiler optimization.</P>
<P><A NAME="Fig2"><!-- </A><A HREF="javascript:displayWindow('images/08-02.jpg',413,265 )"> --><IMG SRC="images/08-02.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/08-02.jpg',413,265)"> --><FONT COLOR="#000077"><B>Figure 8.2</B></FONT></A>&nbsp;&nbsp;<I>Linked array storage format (version 1).</I>
</P>
<P>As it happens, Microsoft C/C<SMALL>&#43;&#43;</SMALL> does optimize the inner loop of <B>FindIDAverage</B> nicely. Listing 8.2 shows the code Microsoft C/C<SMALL>&#43;&#43;</SMALL> generates for the inner loop, consisting of a mere seven assembly language instructions inside the loop. The compiler is smart enough to convert the loop index variable, which counts up but is used for nothing but counting loops, into a count-down variable so that the <B>LOOP</B> instruction can be used.</P>
<P><B>LISTING 8.2 L8-2.COD</B></P>
<!-- CODE //-->
<PRE>
; Code generated by Microsoft C for inner loop of FindIDAverage.
;|*** for (WorkingBlockCount=0;
;|*** WorkingBlockCount&ltBlockPointer-&gtBlockCount;
;|*** WorkingBlockCount&#43;&#43;, DataPointer&#43;&#43;) {
mov WORD PTR [bp-6],0 ;WorkingBlockCount
mov bx,WORD PTR [bp&#43;6] ;BlockPointer
cmp WORD PTR [bx&#43;2],0
je $FB264
mov cx,WORD PTR [bx&#43;2]
add WORD PTR [bp-6],cx ;WorkingBlockCount
mov di,WORD PTR [bp-2] ;IDMatchSum
mov dx,WORD PTR [bp-4] ;IDMatchCount
$L20004:
;|*** if (DataPointer-&gtID == SearchedForID) {
mov ax,WORD PTR [si]
cmp WORD PTR [bp&#43;4],ax ;SearchedForID
jne $I265
;|*** IDMatchCount&#43;&#43;;
inc dx
;|*** IDMatchSum &#43;= DataPointer-&gtValue;
add di,WORD PTR [si&#43;2]
;|*** }
;|*** }
$I265:
add si,4
loop $L20004
mov WORD PTR [bp-2],di ;IDMatchSum
mov WORD PTR [bp-4],dx ;IDMatchCount
$FB264:
</PRE>
<!-- END CODE //-->
<P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="08-02.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="08-04.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

273
ch08/08-04.html Normal file
View file

@ -0,0 +1,273 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Speeding Up C with Assembly Language</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=08//-->
<!--PAGES=160-163//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="08-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="08-05.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>It&#146;s hard to squeeze much more performance from this code by tweaking it, as exemplified by Listing 8.3, a fine-tuned assembly version of <B>FindIDAverage</B> that was produced by looking at the assembly output of MS C/C<SMALL>&#43;&#43;</SMALL> and tightening it. Listing 8.3 eliminates all stack frame access in the inner loop, but that&#146;s about all the tightening there is to do. The result, as shown in Table 8.1, is that Listing 8.3 runs a modest 11 percent faster than Listing 8.1 on a 386. The results could vary considerably, depending on the nature of the data set searched through (average block size and frequency of matches). But, then, understanding the typical and worst case conditions is part of optimization, isn&#146;t it?</P>
<P><B>LISTING 8.3 L8-3.ASM</B></P>
<!-- CODE //-->
<PRE>
; Typically optimized assembly language version of FindIDAverage.
SearchedForID equ 4 ;Passed parameter offsets in the
BlockPointer equ 6 ; stack frame (skip over pushed BP
; and the return address)
NextBlock equ 0 ;Field offsets in struct BlockHeader
BlockCount equ 2
BLOCK_HEADER_SIZE equ 4 ;Number of bytes in struct BlockHeader
ID equ 0 ;struct DataElement field offsets
Value equ 2
DATA_ELEMENT_SIZE equ 4 ;Number of bytes in struct DataElement
.model small
.code
public _FindIDAverage
</PRE>
<!-- END CODE //-->
<TABLE WIDTH="100%">
<TR>
<TD ALIGN="LEFT" VALIGN="TOP" COLSPAN="3"><HR>
<TR>
<TH ALIGN="LEFT" VALIGN="TOP">
<TH ALIGN="LEFT" VALIGN="TOP">On 20 MHz 386
<TH ALIGN="LEFT" VALIGN="TOP">On 10 MHz 286
<TR>
<TD ALIGN="LEFT" VALIGN="TOP" COLSPAN="3"><HR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP"><B>Listing 8.1</B>
<TD ALIGN="LEFT" VALIGN="TOP">294 microseconds
<TD ALIGN="LEFT" VALIGN="TOP">768 microseconds
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">(MSC with maximum optimization)
<TD ALIGN="LEFT" VALIGN="TOP">
<TD ALIGN="LEFT" VALIGN="TOP">
<TR>
<TD ALIGN="LEFT" VALIGN="TOP"><B>Listing 8.3</B>
<TD ALIGN="LEFT" VALIGN="TOP">265
<TD ALIGN="LEFT" VALIGN="TOP">644
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">(Assembly)
<TD ALIGN="LEFT" VALIGN="TOP">
<TD ALIGN="LEFT" VALIGN="TOP">
<TR>
<TD ALIGN="LEFT" VALIGN="TOP"><B>Listing 8.4</B>
<TD ALIGN="LEFT" VALIGN="TOP">212
<TD ALIGN="LEFT" VALIGN="TOP">486
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">(Optimized assembly)
<TD ALIGN="LEFT" VALIGN="TOP">
<TD ALIGN="LEFT" VALIGN="TOP">
<TR>
<TD ALIGN="LEFT" VALIGN="TOP"><B>Listing 8.6</B>
<TD ALIGN="LEFT" VALIGN="TOP">100
<TD ALIGN="LEFT" VALIGN="TOP">207
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">(Optimized assembly with reorganized data)
<TD ALIGN="LEFT" VALIGN="TOP">
<TD ALIGN="LEFT" VALIGN="TOP">
<TR>
<TD ALIGN="LEFT" VALIGN="TOP" COLSPAN="3"><HR>
<TR>
<TH ALIGN="LEFT" VALIGN="TOP">Table 8.1 Execution Times of FindIDAverage.
<TR>
<TD ALIGN="LEFT" VALIGN="TOP" COLSPAN="3"><HR>
<TR>
</TABLE>
<!-- CODE //-->
<PRE>
_FindIDAverage proc near
push bp ;Save caller&#146;s stack frame
mov bp,sp ;Point to our stack frame
push di ;Preserve C register variables
push si
sub dx,dx ;IDMatchSum = 0
mov bx,dx ;IDMatchCount = 0
mov si,[bp&#43;BlockPointer] ;Pointer to first block
mov ax,[bp&#43;SearchedForID] ;ID we&#146;re looking for
; Search through all the linked blocks until the last block
; (marked with a NULL pointer to the next block) has been searched.
BlockLoop:
; Point to the first DataElement entry within this block.
lea di,[si&#43;BLOCK_HEADER_SIZE]
; Search through all the DataElement entries within this block
; and accumulate data from all that match the desired ID.
mov cx,[si&#43;BlockCount]
jcxz DoNextBlock ;No data in this block
IntraBlockLoop:
cmp [di&#43;ID],ax ;Do we have an ID match?
jnz NoMatch ;No match
inc bx ;We have a match; IDMatchCount&#43;&#43;;
add dx,[di&#43;Value] ;IDMatchSum &#43;= DataPointer-&gtValue;
NoMatch:
add di,DATA_ELEMENT_SIZE ;point to the next element
loop IntraBlockLoop
; Point to the next block and continue if that pointer isn&#146;t NULL.
DoNextBlock:
mov si,[si&#43;NextBlock] ;Get pointer to the next block
and si,si ;Is it a NULL pointer?
jnz BlockLoop ;No, continue
; Calculate the average of all matches.
sub ax,ax ;Assume we found no matches
and bx,bx
jz Done ;We didn&#146;t find any matches, return 0
xchg ax,dx ;Prepare for division
div bx ;Return IDMatchSum / IDMatchCount
Done: pop si ;Restore C register variables
pop di
pop bp ;Restore caller&#146;s stack frame
ret
_FindIDAverage ENDP
end
</PRE>
<!-- END CODE //-->
<P>Listing 8.4 tosses some sophisticated optimization techniques into the mix. The loop is unrolled eight times, eliminating a good deal of branching, and <B>SCASW</B> is used instead of <B>CMP [DI],AX.</B> (Note, however, that <B>SCASW</B> is in fact slower than <B>CMP [DI],AX</B> on the 386 and 486, and is sometimes faster on the 286 and 8088 only because it&#146;s shorter and therefore may prefetch faster.) This advanced tweaking produces a 39 percent improvement over the original C code&#151;substantial, but not a tremendous return for the optimization effort invested.</P>
<P><B>LISTING 8.4 L8-4.ASM</B></P>
<!-- CODE //-->
<PRE>
; Heavily optimized assembly language version of FindIDAverage.
; Features an unrolled loop and more efficient pointer use.
SearchedForID equ 4 ;Passed parameter offsets in the
BlockPointer equ 6 ; stack frame (skip over pushed BP
; and the return address)
NextBlock equ 0 ;Field offsets in struct BlockHeader
BlockCount equ 2
BLOCK_HEADER_SIZE equ 4 ;Number of bytes in struct BlockHeader
ID equ 0 ;struct DataElement field offsets
Value equ 2
DATA_ELEMENT_SIZE equ 4 ;Number of bytes in struct DataElement
.model small
.code
public _FindIDAverage
_FindIDAverage proc near
push bp ;Save caller&#146;s stack frame
mov bp,sp ;Point to our stack frame
push di ;Preserve C register variables
push si
mov di,ds ;Prepare for SCASW
mov es,di
cld
sub dx,dx ;IDMatchSum = 0
mov bx,dx ;IDMatchCount = 0
mov si,[bp&#43;BlockPointer] ;Pointer to first block
mov ax,[bp&#43;SearchedForID] ;ID we&#146;re looking for
; Search through all of the linked blocks until the last block
; (marked with a NULL pointer to the next block) has been searched.
BlockLoop:
; Point to the first DataElement entry within this block.
lea di,[si&#43;BLOCK_HEADER_SIZE]
; Search through all the DataElement entries within this block
; and accumulate data from all that match the desired ID.
mov cx,[si&#43;BlockCount] ;Number of elements in this block
jcxz DoNextBlock ;Skip this block if it&#146;s empty
mov bp,cx ;***stack frame no longer available***
add cx,7
shr cx,1 ;Number of repetitions of the unrolled
shr cx,1 ; loop = (BlockCount &#43; 7) / 8
shr cx,1
and bp,7 ;Generate the entry point for the
shl bp,1 ; first, possibly partial pass through
jmp cs:[LoopEntryTable&#43;bp] ; the unrolled loop and
; vector to that entry point
align 2
LoopEntryTable label word
dw LoopEntry8,LoopEntry1,LoopEntry2,LoopEntry3
dw LoopEntry4,LoopEntry5,LoopEntry6,LoopEntry7
M_IBL macro P1
local NoMatch
LoopEntry&ampP1&amp:
scasw ;Do we have an ID match?
jnz NoMatch ;No match
;We have a match
inc bx ;IDMatchCount&#43;&#43;;
add dx,[di] ;IDMatchSum &#43;= DataPointer-&gtValue;
NoMatch:
add di,DATA_ELEMENT_SIZE-2 ;point to the next element
; (SCASW advanced 2 bytes already)
endm
align 2
IntraBlockLoop:
M_IBL 8
M_IBL 7
M_IBL 6
M_IBL 5
M_IBL 4
M_IBL 3
M_IBL 2
M_IBL 1
loop IntraBlockLoop
; Point to the next block and continue if that pointer isn&#146;t NULL.
DoNextBlock:
mov si,[si&#43;NextBlock] ;Get pointer to the next block
and si,si ;Is it a NULL pointer?
jnz BlockLoop ;No, continue
; Calculate the average of all matches.
sub ax,ax ;Assume we found no matches
and bx,bx
jz Done ;We didn&#146;t find any matches, return 0
xchg ax,dx ;Prepare for division
div bx ;Return IDMatchSum / IDMatchCount
Done: pop si ;Restore C register variables
pop di
pop bp ;Restore caller&#146;s stack frame
ret
_FindIDAverage ENDP
end
</PRE>
<!-- END CODE //-->
<P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="08-03.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="08-05.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

220
ch08/08-05.html Normal file
View file

@ -0,0 +1,220 @@
<HTML>
<HEAD>
<META name=vsisbn content="1576101746">
<META name=vstitle content="Michael Abrash's Graphics Programming Black Book, Special Edition">
<META name=vsauthor content="Michael Abrash">
<META name=vspublisher content="The Coriolis Group">
<META name=vspubdate content="07/01/97">
<META name=vscategory content="Web and Software Development: Game Development,Web and Software Development: Graphics and Multimedia Development">
<TITLE>Michael Abrash's Graphics Programming Black Book Special Edition: Speeding Up C with Assembly Language</TITLE>
<!-- HEADER -->
<!-- Empty Reference Subhead -->
<!--ISBN=1576101746//-->
<!--TITLE=Michael Abrash's Graphics Programming Black Book Special Edition//-->
<!--AUTHOR=Michael Abrash//-->
<!--PUBLISHER=The Coriolis Group, Inc.//-->
<!--CHAPTER=08//-->
<!--PAGES=163-166//-->
<!--UNASSIGNED1//-->
<!--UNASSIGNED2//--></HEAD><BODY LINK=#0000FF ALINK=#000099 VLINK=#0000FF BGCOLOR=#FFFFFF>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="08-04.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch09/09-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<P><BR></P>
<P>Listings 8.5 and 8.6 together go the final step and change the rules in favor of assembly language. Listing 8.5 creates the same list of linked blocks as Listing 8.1. However, instead of storing an array of structures within each block, it stores <I>two</I> arrays in each block, one consisting of ID numbers and the other consisting of the corresponding values, as shown in Figure 8.3. No information is lost; the data is merely rearranged.</P>
<P><B>LISTING 8.5 L8-5.C</B></P>
<!-- CODE //-->
<PRE>
/* Program to search an array spanning a linked list of variable-
sized blocks, for all entries with a specified ID number,
and return the average of the values of all such entries. Each of
the variable-sized blocks may contain any number of data entries,
stored in the form of two separate arrays, one for ID numbers and
one for values. */
#include &ltstdio.h&gt
#ifdef __TURBOC__
#include &ltalloc.h&gt
#else
#include &ltmalloc.h&gt
#endif
void main(void);
void exit(int);
extern unsigned int FindIDAverage2(unsigned int,
struct BlockHeader *);
</PRE>
<!-- END CODE //-->
<P><A NAME="Fig3"><!-- </A><A HREF="javascript:displayWindow('images/08-03.jpg',413,261 )"> --><IMG SRC="images/08-03.jpg"><BR><!-- </A>
<BR><A HREF="javascript:displayWindow('images/08-03.jpg',413,261)"> --><FONT COLOR="#000077"><B>Figure 8.3</B></FONT></A>&nbsp;&nbsp;<I>Linked array storage format (version 2).</I>
</P>
<!-- CODE //-->
<PRE>
/* Structure that starts each variable-sized block */
struct BlockHeader {
struct BlockHeader *NextBlock; /* Pointer to next block, or NULL
if this is the last block in the
linked list */
unsigned int BlockCount; /* The number of DataElement entries
in this variable-sized block */
};
void main(void) {
int i,j;
unsigned int IDToFind;
struct BlockHeader *BaseArrayBlockPointer,*WorkingBlockPointer;
int *WorkingDataPointer;
struct BlockHeader **LastBlockPointer;
printf(&#148;ID # for which to find average: &#147;);
scanf(&#148;%d&#148;,&ampIDToFind);
/* Build an array across 5 blocks, for testing */
/* Anchor the linked list to BaseArrayBlockPointer */
LastBlockPointer = &ampBaseArrayBlockPointer;
/* Create 5 blocks of varying sizes */
for (i = 1; i &lt 6; i&#43;&#43;) {
/* Try to get memory for the next block */
if ((WorkingBlockPointer =
(struct BlockHeader *) malloc(sizeof(struct BlockHeader) &#43;
sizeof(int) * 2 * i * 10)) == NULL) {
exit(1);
}
/* Set the number of data elements in this block */
WorkingBlockPointer-&gtBlockCount = i * 10;
/* Link the new block into the chain */
*LastBlockPointer = WorkingBlockPointer;
/* Point to the first data field */
WorkingDataPointer = (int *) ((char *)WorkingBlockPointer &#43;
sizeof(struct BlockHeader));
/* Fill the data fields with ID numbers and values */
for (j = 0; j &lt (i * 10); j&#43;&#43;, WorkingDataPointer&#43;&#43;) {
*WorkingDataPointer = j;
*(WorkingDataPointer &#43; i * 10) = i * 1000 &#43; j;
}
/* Remember where to set link from this block to the next */
LastBlockPointer = &ampWorkingBlockPointer-&gtNextBlock;
}
/* Set the last block&#146;s &#147;next block&#148; pointer to NULL to indicate
that there are no more blocks */
WorkingBlockPointer-&gtNextBlock = NULL;
printf(&#148;Average of all elements with ID %d: %u\n&#148;,
IDToFind, FindIDAverage2(IDToFind, BaseArrayBlockPointer));
exit(0);
}
</PRE>
<!-- END CODE //-->
<P><B>LISTING 8.6 L8-6.ASM</B></P>
<!-- CODE //-->
<PRE>
; Alternative optimized assembly language version of FindIDAverage
; requires data organized as two arrays within each block rather
; than as an array of two-value element structures. This allows the
; use of REP SCASW for ID searching.
SearchedForIDequ4 ;Passed parameter offsets in the
BlockPointerequ6 ; stack frame (skip over pushed BP
; and the return address)
NextBlockequ0 ;Field offsets in struct BlockHeader
BlockCountequ2
BLOCK_HEADER_SIZEequ4 ;Number of bytes in struct BlockHeader
.model small
.code
public _FindIDAverage2
_FindIDAverage2 proc near
push bp ;Save caller&#146;s stack frame
mov bp,sp ;Point to our stack frame
push di ;Preserve C register variables
push si
mov di,ds ;Prepare for SCASW
mov es,di
cld
mov si,[bp&#43;BlockPointer] ;Pointer to first block
mov ax,[bp&#43;SearchedForID] ;ID we&#146;re looking for
sub dx,dx ;IDMatchSum = 0
mov bp,dx ;IDMatchCount = 0
;***stack frame no longer available***
; Search through all the linked blocks until the last block
; (marked with a NULL pointer to the next block) has been searched.
BlockLoop:
; Search through all the DataElement entries within this block
; and accumulate data from all that match the desired ID.
mov cx,[si&#43;BlockCount]
jcxz DoNextBlock;Skip this block if there&#146;s no data
; to search through
mov bx,cx ;We&#146;ll use BX to point to the
shl bx,1 ; corresponding value entry in the
; case of an ID match (BX is the
; length in bytes of the ID array)
; Point to the first DataElement entry within this block.
lea di,[si&#43;BLOCK_HEADER_SIZE]
IntraBlockLoop:
repnz scasw ;Search for the ID
jnz DoNextBlock ;No match, the block is done
inc bp ;We have a match; IDMatchCount&#43;&#43;;
add dx,[di&#43;bx-2];IDMatchSum &#43;= DataPointer-&gtValue;
; (SCASW has advanced DI 2 bytes)
and cx,cx ;Is there more data to search through?
jnz IntraBlockLoop ;yes
; Point to the next block and continue if that pointer isn&#146;t NULL.
DoNextBlock:
mov si,[si&#43;NextBlock] ;Get pointer to the next block
and si,si ;Is it a NULL pointer?
jnz BlockLoop ;No, continue
; Calculate the average of all matches.
sub ax,ax ;Assume we found no matches
and bp,bp
jz Done ;We didn&#146;t find any matches, return 0
xchg ax,dx ;Prepare for division
div bp ;Return IDMatchSum / IDMatchCount
Done: pop si ;Restore C register variables
pop di
pop bp ;Restore caller&#146;s stack frame
ret
_FindIDAverage2 ENDP
end
</PRE>
<!-- END CODE //-->
<P>The whole point of this rearrangement is to allow us to use <B>REP SCASW</B> to search through each block, and that&#146;s exactly what <B>FindIDAverage2</B> in Listing 8.6 does. The result: Listing 8.6 calculates the average about <I>three times</I> as fast as the original C implementation and more than twice as fast as Listing 8.4, heavily optimized as the latter code is.</P>
<P>I trust you get the picture. The sort of instruction-by-instruction optimization that so many of us love to do as a kind of puzzle is fun, but compilers can do it nearly as well as you can, and in the future will surely do it better. What a compiler <I>can&#146;t</I> do is tie together the needs of the program specification on the high end and the processor on the low end, resulting in critical code that runs just about as fast as the hardware permits. The only software that can do that is located north of your sternum and slightly aft of your nose. Dust it off and put it to work&#151;and your code will never again be confused with anything by Hamilton, Joe, Frank, eynolds or Bo Donaldson and the Heywoods.</P><P><BR></P>
<CENTER>
<TABLE BORDER>
<TR>
<TD><A HREF="08-04.html">Previous</A></TD>
<TD><A HREF="../index.html">Table of Contents</A></TD>
<TD><A HREF="../ch09/09-01.html">Next</A></TD>
</TR>
</TABLE>
</CENTER>
<hr width="90%" size="1" noshade>
<div align="center">
<font face="Verdana,sans-serif" size="1">Graphics Programming Black Book &copy; 2001 Michael Abrash</font>
</div>
<!-- all of the reference materials (books) have the footer and subfoot reveresed -->
<!-- reference_subfoot = footer -->
<!-- reference_footer = subfoot -->
<!-- BEGIN SUB FOOTER -->
</BODY>
</HTML>
<!-- END FOOTER -->

BIN
ch08/images/08-01.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
ch08/images/08-01i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch08/images/08-02.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
ch08/images/08-02i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
ch08/images/08-03.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
ch08/images/08-03i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Some files were not shown because too many files have changed in this diff Show more