Programming/Software Engineering
The Practice of Programming With the same insight and authority that made their book ...
441 downloads
3198 Views
5MB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
Programming/Software Engineering
The Practice of Programming With the same insight and authority that made their book The Unix Programming Environment a classic, Brian Kernighan and Rob Pike have written The Practice o f Programming to help make individual programmers more effective a n d productive. The practice of programming is more than just writing code. Programmers must also assess tradeoffs, choose among design alternatives, debug and test, improve performance, and maintain software written by themselves and others. At the same time, they must be concerned with issues like compatibility, robustness, and reliability, while meeting specifications. The Practice o f Programming covers all these topics, and more. This book i s full of practical advice and real-world examples in C, C++, lava, and a variety of special-purpose languages. It includes chapters on: debugging: finding bugs quickly and methodically testing: guaranteeing that software works correctly and reliably performance: making programs faster and more compact portability: ensuring that programs run everywhere without change design: balancing goals and constraints to decide which algorithms and data structures are best interfaces: using abstraction and information hiding to control the interactions between components style: writing code that works well and is a pleasure to read notation: choosing languages and tools that let the machine do more of the work Kernighan a n d Pike have distilled years o f experience w r i t i n g programs, teaching, and working with other programmers to create this book. Anyone who writes software will profit from the principles and guidance i n The Practice o f Programming.
Brian W. Kernighan and Rob Pike work i n the Computing Science Research Center at Bell Laboratories, Lucent Technologies. Brian Kernighan is Consulting Editor for Addison-Wesley's Professional Computing Series and the author, with Dennis Ritchie, of The C Programming Language. Rob Pike was a lead architect and implementer of the Plan 9 and Inferno operating systems. His research focuses on software that makes it easier for people to write software
Cover art by Renee French QText printed on recycled paper h ADDISON-WESLEY Addison-Wesley is an imprint of Addison Wesley Longman, Inc.
The Practice of Programming
Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and Addison Wesley Longman, Inc. was aware of a trademark claim. the designations have been printed in initial capital letters or all capital letters. The authors and publisher have taken care in preparation of this book, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein, The publisher offers discounts of this book when ordered in quantity for special sales. For more information, please contact: Computer and Engineering Publishing Group Addison Wesley Longman, Inc. One Jacob Way Reading, Massachusetts 01867
This book was typeset (gri~l~l)icltI)Ilqnlt~nff -nip) in Times and Lucida Sans Typewriter by the authors.
Library of Congress Cataloging-in-Publication Data
Kernighan, Brian W. The practice of programming 1 Brian W. Kernighan, Rob Pike. p. cm. -- (Addison-Wesley professional computing series) Includes bibliographical references. ISBN 0-201-61586-X 1. Computer programming. I. Pike, Rob. 11. Title. 111. Series. QA76.6 .K48 1999 005.1--dc21 99-10131 CIP
Copyright O 1999 by Lucent Technologies. All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted. in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise, without the prior written permission of the publisher. Printed in the United States of America. Published simultaneously in Canada. Text printed on recycled and acid-free paper.
ISBN 0-201-61586-X 2 3 4 5 6 7 CRS 02010099 2nd Printing May 1999
Contents
Preface
Chapter 1: Style 1.1 Names 1.2 Expressions and Statements 1.3 Consistency and Idioms 1.4 Function Macros 1.5 Magic Numbers 1.6 Comments 1.7 Why Bother? Chapter 2: Algorithms and Data Structures 2.1 Searching 2.2 Sorting 2.3 Libraries 2.4 A Java Quicksort 2.5 0-Notation 2.6 Growing Arrays 2.7 Lists 2.8 Trees 2.9 Hash Tables 2.10 Summary
Chapter 3: Design and Implementation 3.1 The Markov Chain Algorithm 3.2 Data Structure Alternatives 3.3 Building the Data Suucture in C 3.4 Generating Output
3.5 3.6 3.7 3.8 3.9
Java C++ AwkandPerl Performance Lessons
Chapter 4: Interfaces 4.1 Comma-Separated Values 4.2 A Prototype Library 4.3 A Library for Others 4.4 A C++ Implementation 4.5 Interface Principles 4.6 Resource Management 4.7 Abort, Retry. Fail? 4.8 User Interfaces Chapter 5: Debugging 5.1 Debuggers 5.2 Good Clues, Easy Bugs 5.3 No Clues, Hard Bugs 5.4 Last Resorts 5.5 Non-reproducible Bugs 5.6 Debugging Tools 5.7 Other People's Bugs 5.8 Summary Chapter 6: Testing 6.1 Test as You Write the Code 6.2 Systematic Testing 6.3 Test Automation 6.4 Test Scaffolds 6.5 Stress Tests 6.6 Tips for Testing 6.7 Who Does the Testing? 6.8 Testing the Markov Program 6.9 Summary Chapter 7: Performance 7.1 A Bottleneck 7.2 Timing and Profiling 7.3 Strategies for Speed 7.4 Tuning the Code 7.5 Space Efficiency
7.6 Estimation 7.7 Summary
Chapter 8: Portability 8.1 Language 8.2 Headers and Libraries 8.3 Program Organization 8.4 Isolation 8.5 Data Exchange 8.6 Byte Order 8.7 Portability and Upgrade 8.8 Internationalization 8.9 Summary Chapter 9: Notation 9.1 Formatting Data 9.2 Regular Expressions 9.3 Programmable Tools 9.4 Interpreters, Compilers, and Virtual Machines 9.5 Programs that Write Programs 9.6 Using Macros to Generate Code 9.7 Compiling on the Fly Epilogue Appendix: Collected Rules Index
Preface Have you ever... wasted a lot of time coding the wrong algorithm? used a data structure that was much too complicated? tested a program but missed an obvious problem? spent a day looking for a bug you should have found in five minutes? needed to make a program run three times faster and use less memory? struggled to move a program from a workstation to a PC or vice versa? tried to make a modest change in someone else's program? rewritten a program because you couldn't understand it? Was it fun? These things happen to programmers all the time. But dealing with such problems is often harder than it should be because topics like testing, debugging, portability, performance, design alternatives, and style-the practice of programming-are not usually the focus of computer science or programming courses. Most programmers learn them haphazardly as their experience grows, and a few never learn them at all. In a world of enormous and intricate interfaces, constantly changing tools and languages and systems, and relentless pressure for more of everything, one can lose sight of the basic principles-simplicity, clarity, generality-that form the bedrock of good software. One can also overlook the value of tools and notations that mechanize some of software creation and thus enlist the computer in its own programming. Our approach in this book is based on these underlying, interrelated principles, which apply at all levels of computing. These include simpliciry, which keeps programs short and manageable; clariry, which makes sure they are easy to understand, for people as well as machines; generality, which means they work well in a broad range of situations and adapt well as new situations arise; and automation, which lets the machine do the work for us, freeing us from mundane tasks. By looking at computer programming in a variety of languages, from algorithms and data structures through design, debugging, testing, and performance improvement, we can illustrate
X
PREFACE
universal engineering concepts that are independent of language. operating system, or programming paradigm. This book comes from many years of experience writing and maintaining a lot of software, teaching programming courses, and working with a wide variety of programmers. We want to share lessons about practical issues. to pass on insights from our experience, and to suggest ways for programmers of all levels to be more proficient and productive. We are writing for several kinds of readers. If you are a student who has taken a programming course or two and would like to be a better programmer, this book will expand on some of the topics for which there wasn't enough time in school. If you write programs as part of your work, but in support of other activities rather than as the goal in itself, the information will help you to program more effectively. If you are a professional programmer who didn't get enough exposure to such topics in school or who would like a refresher, or if you are a software manager who wants to guide your staff in the right direction, the material here should be of value. We hope that the advice will help you to write better programs. The only prerequisite is that you have done some programming, preferably in C. C++ or Java. Of course the more experience you have, the easier it will be; nothing can take you from neophyte to expert in 21 days. Unix and Linux programmers will find some of the examples more familiar than will those who have used only Windows and Macintosh systems, but programmers from any environment should discover things to make their lives easier. The presentation is organized into nine chapters, each focusing on one major aspect of programming practice. Chapter 1 discusses programming style. Good style is so important to good programming that we have chosen to cover it first. Well-written programs are better than badly-written ones-they have fewer errors and are easier to debug and to modifyso it is important to think about style from the beginning. This chapter also introduces an important theme in good programming, the use of idioms appropriate to the language being used. Algorithms and data structures. the topics of Chapter 2, are the core of the computer science curriculum and a major part of programming courses. Since most readers will already be familiar with this material, our treatment is intended as a brief review of the handful of algorithms and data structures that show up in almost every program. More complex algorithms and data structures usually evolve from these building blocks, so one should master the basics. Chapter 3 describes the design and implementation of a small program that illustrates algorithm and data structure issues in a realistic setting. The program is implemented in five languages; comparing the versions shows how the same data structures are handled in each, and how expressiveness and performance vary across a spectrum of languages.
Interfaces between users, programs, and parts of programs are fundamental in programming and much of the success of software is determined by how well interfaces are designed and implemented. Chapter 4 shows the evolution of a small library for parsing a widely used data format. Even though the example is small. it illustrates many of the concerns of interface design: abstraction, information hiding, resource management, and error handling. Much as we try to write programs correctly the first time, bugs, and therefore debugging, are inevitable. Chapter 5 gives strategies and tactics for systematic and effective debugging. Among the topics are the signatures of common bugs and the importance of "numerology," where patterns in debugging output often indicate where a problem lies. Testing is an attempt to develop a reasonable assurance that a program is working correctly and that it stays correct as it evolves. The emphasis in Chapter 6 is on systematic testing by hand and machine. Boundary condition tests probe at potential weak spots. Mechanization and test scaffolds make it easy to do extensive testing with modest effort. Stress tests provide a different kind of testing than typical users do and ferret out a different class of bugs. Computers are so fast and compilers are so good that many programs are fast enough the day they are written. But others are too slow, or they use too much memory, or both. Chapter 7 presents an orderly way to approach the task of making a program use resources efficiently, so that the program remains correct and sound as it is made more efficient. Chapter 8 covers portability. Successful programs live long enough that their environment changes, or they must be moved to new systems or new hardware or new countries. The goal of portability is to reduce the maintenance of a program by minimizing the amount of change necessary to adapt it to a new environment. Computing is rich in languages, not just the general-purpose ones that we use for the bulk of programming, but also many specialized languages that focus on narrow domains. Chapter 9 presents several examples of the importance of notation in computing, and shows how we can use it to simplify programs, to guide implementations, and even to help us write programs that write programs. To talk about programming, we have to show a lot of code. Most of the examples were written expressly for the book, although some small ones were adapted from other sources. We've tried hard to write our own code well, and have tested it on half a dozen systems directly from the machine-readable text. More information is available at the web site for The Practice of Programming:
The majority of the programs are in C, with a number of examples in C++ and Java and some brief excursions into scripting languages. At the lowest level, C and C++ are almost identical and our C programs are valid C++ programs as well. C++ and Java are lineal descendants of C, sharing more than a little of its syntax and much of its efficiency and expressiveness, while adding richer type systems and libraries.
xii
PREFACE
In our own work, we routinely use all three of these languages, and many others. The choice of language depends on the problem: operating systems are best written in an efficient and unrestrictive language like C or C u ; quick prototypes are often easiest in a command interpreter or a scripting language like Awk or Perl; for user interfaces. Visual Basic and Tcmk are strong contenders, along with Java. There is an important pedagogical issue in choosing a language for our examples. Just as no language solves all problems equally well, no single language is best for presenting all topics. Higher-level languages preempt some design decisions. If we use a lower-level language, we get to consider alternative answers to the questions; by exposing more of the details, we can talk about them better. Experience shows that even when we use the facilities of high-level languages, it's invaluable to know how they relate to lower-level issues; without that insight, it's easy to run into performance problems and mysterious behavior. So we will often use C for our examples, even though in practice we might choose something else. For the most part, however, the lessons are independent of any particular programming language. The choice of data structure is affected by the language at hand; there may be few options in some languages while others might support a variety of alternatives. But the way to approach making the choice will be the same. The details of how to test and debug are different in different languages, but strategies and tactics are similar in all. Most of the techniques for making a program efficient can be applied in any language. Whatever language you write in, your task as a programmer is to do the best you can with the tools at hand. A good programmer can overcome a poor language or a clumsy operating system, but even a great programming environment will not rescue a bad programmer. We hope that, no matter what your current experience and skill. this book will help you to program better and enjoy it more. We are deeply grateful to friends and colleagues who read drafts of the manuscript and gave us many helpful comments. Jon Bentley. Russ Cox. John Lakos. John Linderman, Peter Memishian, lan Lance Taylor, Howard Trickey, and Chris Van Wyk read the manuscript, some more than once, with exceptional care and thoroughness. We are indebted to Tom Cargill, Chris Cleeland, Steve Dewhurst, Eric Grosse, Andrew Herron. Gerard Holzmann, Doug McIlroy. Paul McNamee, Peter Nelson, Dennis Ritchie, Rich Stevens, Tom Szymanski, Kentaro Toyama, John Wait, Daniel C. Wang, Peter Weinberger. Margaret Wright. and Cliff Young for invaluable comments on drafts at various stages. We also appreciate good advice and thoughtful suggestions from A1 Aho, Ken Arnold, Chuck Bigelow, Joshua Bloch. Bill Coughran. Bob Flandrena, Renee French, Mark Kernighan. Andy Koenig, Sape Mullender. Evi Nemeth, Many Rabinowitz, Mark V. Shaney, Bjarne Stroustrup, Ken Thompson, and Phil Wadler. Thank you all.
Brian W. Kernighan Rob Pike
Style It is an old observation that the best writers sometimes disregard the rules of rhetoric. When they do so, however, the reader will usually find in the sentence some compensating merit, attained at the cost of the violation. Unless he is certain of doing as well, he will probably do best to follow the rules.
William Strunk and E. B. White, The Elements of Sryle This fragment of code comes from a large program written many years ago: i f ( (country == SING) I I (country == BRNI) I I (country == POL) I I (country == ITALY) ) C
/* * I f t h e country i s Singapore, Brunei o r Poland * then t h e current time i s t h e answer time * rather than t h e o f f hook time. * Reset answer time and s e t day of week. */
It's carefully written. formatted, and commented, and the program it comes from works extremely well; the programmers who created this system are rightly proud of what they built. But this excerpt is puzzling to the casual reader. What relationship links Singapore, Brunei, Poland and Italy? Why isn't Italy mentioned in the comment? Since the comment and the code differ, one of them must be wrong. Maybe both are. The code is what gets executed and tested, so it's more likely to be right; probably the comment didn't get updated when the code did. The comment doesn't say enough about the relationship among the three countries it does mention; if you had to maintain this code, you would need to know more. The few lines above are typical of much real code: mostly well done, but with some things that could be improved.
2
STYLE
CHAPTER 1
This book is about the practice of programming-how to write programs for real. Our purpose is to help you to write software that works at least as well as the program this example was taken from, while avoiding trouble spots and weaknesses. We will talk about writing better code from the beginning and improving it as it evolves. We are going to start in an unusual place, however, by discussing programming style. The purpose of style is to make the code easy to read for yourself and others, and good style is crucial to good programming. We want to talk about it first so you will be sensitive to it as you read the code in the rest of the book. There is more to writing a program than getting the syntax right, fixing the bugs, and making it run fast enough. Programs are read not only by computers but also by programmers. A well-written program is easier to understand and to modify than a poorly-written one. The discipline of writing well leads to code that is more likely to be correct. Fortunately, this discipline is not hard. The principles of programming style are based on common sense guided by experience, not on arbitrary rules and prescriptions. Code should be clear and simplestraightforward logic, natural expression, conventional language use, meaningful names, neat formatting, helpful comments-and it should avoid clever tricks and unusual constructions. Consistency is important because others will find it easier to read your code, and you theirs, if you all stick to the same style. Details may be imposed by local conventions, management edict, or a program, but even if not, it is best to obey a set of widely shared conventions. We follow the style used in the book The C Programming Language, with minor adjustments for C++ and Java. We will often illustrate rules of style by small examples of bad and good programming, since the contrast between two ways of saying the same thing is instructive. These examples are not artificial. The "bad" ones are all adapted from real code, written by ordinary programmers (occasionally ourselves) working under the common pressures of too much work and too little time. Some will be distilled for brevity. but they will not be misrepresented. Then we will rewrite the bad excerpts to show how they could be improved. Since they are real code, however, they may exhibit multiple problems. Addressing every shortcoming would take us too far off topic, so some of the good examples will still harbor other, unremarked flaws. To distinguish bad examples from good, throughout the book we will place question marks in the margins of questionable code, as in this real excerpt: ?
? ?
#define ONE 1 #define TEN 10 #define TWENTY 2 0
Why are these #defines questionable? Consider the modifications that will be necessary if an array of TWENTY elements must be made larger. At the very least, each name should be replaced by one that indicates the role of the specific value in the program: #def i ne INPUT-MODE 1 #define INPUT-BUFSIZE 10 #def ine OUTPUT-BUFSIZE 2 0
NAMES
SECTION 1 .I
3
1.1 Names What's in a name? A variable or function name labels an object and conveys information about its purpose. A name should be informative, concise, memorable, and pronounceable if possible. Much information comes from context and scope; the broader the scope of a variable, the more information should be conveyed by its name.
Use descriptive names for globals, short names for locals. Global variables, by definition, can crop up anywhere in a program, so they need names long enough and descriptive enough to remind the reader of their meaning. It's also helpful to include a brief comment with the declaration of each global: i n t npending = 0 ;
// c u r r e n t length of input queue
Global functions,.classes, and structures should also have descriptive names that suggest their role in a program. By contrast, shorter names suffice for local variables; within a function, n may be sufficient, npoi n t s is fine, and numberof Poi n t s is overkill. Local variables used in conventional ways can have very short names. The use of i and j for loop indices, p and q for pointers, and s and t for strings is so frequent that there is little profit and perhaps some loss in longer names. Compare ? ? ?
f o r (theElementIndex = 0 ; theElementIndex < number0fElements; theElementIndex++) elementArray[theElementIndex] = theElementIndex;
f o r (i = 0 ; i < nelems; i++) elem[i] = i ; Programmers are often encouraged to use long variable names regardless of context. That is a mistake: clarity is often achieved through brevity. There are many naming conventions and local customs. Common ones include using names that begin or end with p, such as nodep, for pointers; initial capital letters for Global s; and all capitals for CONSTANTS. Some programming shops use more sweeping rules, such as notation to encode type and usage information in the variable. perhaps pch to mean a pointer to a character and strTo and strFrom to mean strings that will be written to and read from. As for the spelling of the names themselves, whether to use npendi ng or numPendi ng or num-pendi ng is a matter of taste; specific rules are much less important than consistent adherence to a sensible convention. Naming conventions make it easier to understand your own code. as well as code written by others. They also make it easier to invent new names as the code is being written. The longer the program, the more important is the choice of good. descriptive, systematic names. Namespaces in C++ and packages in Java provide ways to manage the scope of names and help to keep meanings clear without unduly long names.
4
CHAPTER 1
STYLE
Be consistent. Give related things related names that show their relationship and highlight their difference. Besides being much too long, the member names in this Java class are wildly inconsistent: ? ? ? ?
c l a s s UserQueue C i n t noOfIternsInQ, frontOiTheQueue, queuecapacity; public i n t noOfUsersInQueue() { . . . I 3
The word "queue" appears as Q. Queue and queue. But since queues can only be accessed from a variable of type UserQueue, member names do not need to mention "queue" at all; context suffices, so
is redundant. This version is better: c l a s s UserQueue 1 i n t ni terns, f r o n t , capacity; public i n t n u s e r s 0 C. . .) 3 since it leads to statements like
No clarity is lost. This example still needs work, however: "items" and "users" are the same thing, so only one term should be used for a single concept.
Use active names for functions. Function names should be based on active verbs, perhaps followed by nouns: now = date .getTirne() ; putchar('\nl) ; Functions that return a boolean (true or false) value should be named so that the return value is unambiguous. Thus
does not indicate which value is true and which is false, while i f (i soctal (c))
.. .
makes it clear that the function returns true if the argument is octal and false if not.
Be accurate. A name not only labels, it conveys information to the reader. A misleading name can result in mystifying bugs. One of us wrote and distributed for years a macro called i soctal with this incorrect implementation:
SECTION 1.1
?
NAMES
5
#define i s o c t a l ( c ) ((c) >= '0' && (c) ) have higher precedence than the logical operators (&& and I I ). When mixing unrelated operators, though, it's a good idea to parenthesix. C and its friends present pernicious precedence problems, and it's easy to make a mistake.
SECTION 1.2
EXPRESSIONS AND STATEMENTS
7
Because the logical operators bind tighter than assignment, parentheses are mandatory for most expressions that combine them: w h i l e ((c = getchar())
...
!= EOF)
The bitwise operators & and I have lower precedence than relational operators like ==, so despite its appearance, ?
?
i f (x&MASK == BITS)
...
actually means
which is certainly not the programmer's intent. Because it combines bitwise and relational operators, the expression needs parentheses: i f ((x&MASK) == BITS)
...
Even if parentheses aren't necessary, they can help if the grouping is hard to grasp at first glance. This code doesn't need parentheses: ?
leap- year = y % 4 == 0 && y % 100 != 0
I) y
% 400 == 0;
but they make it easier to understand:
We also removed some of the blanks: grouping the operands of higher-precedence operators helps the reader to see the structure more quickly.
Break up complex expressions. C , C++, and Java have rich expression syntax and operators, and it's easy to get carried away by cramming everything into one construction. An expression like the following is compact but it packs too many operations into a single statement:
It's easier to grasp when broken into several pieces: i f (2kk < n-m) axp = c [ k + l ] ; else *xp = d [k--1 ; *x += *xp;
Be clear. Programmers' endless creative energy is sometimes used to write the most concise code possible, or to find clever ways to achieve a result. Sometimes these skills are misapplied, though, since the goal is to write clear code, not clever code.
CHAPTER 1
What does this intricate calculation do? ?
subkey = subkey >> ( b i t o f f
-
((bitoff
>> 3) > ( b i t o f f & 0x7);
It takes a while to puzzle out what the first version is doing; the second is shorter and clearer. Experienced programmers make it even shorter by using an assignment operator: subkey >>= b i t o f f & 0x7;
Some constructs seem to invite abuse. The ?: operator can lead to mysterious code:
It's almost impossible to figure out what this does without following all the possible paths through the expression. This form is longer, but much easier to follow because it makes the paths explicit: i f (LC == 0 child = e l s e i f (LC child = else child =
&& RC == 0) 0; == 0) RC; LC;
The ?: operator is fine for short expressions where it can replace four lines of if-else with one, as in max = (a > b) ? a : b ;
or perhaps p r i n t f ("The l i s t has %d item%s\n", n , n = = l ? "" : "s");
but it is not a general replacement for conditional statements. Clarity is not the same as brevity. Often the clearer code will be shorter, as in the bit-shifting example, but it can also be longer, as in the conditional expression recast as an if-else. The proper criterion is ease of understanding. B e careful with side effects. Operators like ++ have side effects: besides returning a value, they also modify an underlying variable. Side effects can be extremely convenient, but they can also cause trouble because the actions of retrieving the value and updating the variable might not happen at the same time. In C and C++, the order of
SECTION 1.2
EXPRESSIONS AND STATEMENTS
9
execution of side effects is undefined, so this multiple assignment is likely to produce the wrong answer:
The intent is to store blanks at the next two positions in s t r . But depending on when i is updated, a position in s t r could be skipped and i might end up increased only by 1. Break it into two statements:
Even though it contains only one increment, this assignment can also give varying results:
If i is initially 3, the array element might be set to 3 or 4. It's not just increments and decrements that have side effects; I/0 is another source of behind-the-scenes action. This example is an attempt to read two related numbers from standard input:
It is broken because part of the expression modifies yr and another part uses it. The value of p r o f i t [yr] can never be right unless the new value of yr is the same as the old one. You might think that the answer depends on the order in which the arguments are evaluated, but the real issue is that all the arguments to scanf are evaluated before the routine is called, so &profit[yr] will always be evaluated using the old value of yr. This sort of problem can occur in almost any language. The fix is, as usual, to break up the expression:
.
scanf ("%dm &yr) ; scanf ("%dm,& p r o f i t[yr]) ; Exercise caution in any expression with side effects.
Exercise 1-4. Improve each of these fragments:
?
length
=
?
flag
f l a g ? 0 : 1;
=
(length < BUFSIZE) ? length : BUFSIZE;
10
?
? ? ?
STYLE
CHAPTER 1
i f .(val & 1) b i t = 1; else b i t = 0;
Exercise 1-5. What is wrong with this excerpt? ?
? ? ? ? ?
i n t read(int * i p ) { scanf ("%dU, ip) ; return *ip;
1
...
i nsert(&graph[vertl
, read(&val) , read(&ch))
;
Exercise 1-6. List all the different outputs this could produce with various orders of evaluation: ?
?
n = l ; p r i n t f ("%d %d\nM, n++, n++);
Try it on as many compilers as you can, to see what happens in practice.
1.3 Consistency and Idioms Consistency leads to better programs. If formatting varies unpredictably, or a loop over an array runs uphill this time and downhill the next, or strings are copied with strcpy here and a f o r loop there, the variations make it harder to see what's really going on. But if the same computation is done the same way every time it appears, any variation suggests a genuine difference, one worth noting.
Use a consistent indentation and brace style. Indentation shows structure, but which indentation style is best? Should the opening brace go on the same line as the i f or on the next? Programmers have always argued about the layout of programs, but the specific style is much less important than its consistent application. Pick one style, preferably ours, use it consistently, and don't waste time arguing. Should you include braces even when they are not needed? Like parentheses, braces can resolve ambiguity and occasionally make the code clearer. For consistency, many experienced programmers always put braces around loop or i f bodies. But if the body is a single statement they are unnecessary, so we tend to omit them. If you also choose to leave them out, make sure you don't drop them when they are needed to resolve the "dangling else" ambiguity exemplified by this excerpt:
CONSISTENCY AND IDIOMS
SECTION 1.3
?
i f (month==FEB) { i f (year%4 == 0) i f (day > 29) l e g a l = FALSE; e l se i f (day > 28) l e g a l = FALSE;
?
1
? ? ? ?
? ?
11
The indentation is misleading, since the e l s e is actually attached to the line i f (day > 29)
?
and the code is wrong. When one i f immediately follows another, always use braces: ? 7
? ? ?
?
i f (month==FEB) i f (year%4 == 0) { i f (day > 29) 1 egal = FALSE; 1 else { i f (day > 28)
Syntax-driven editing tools make this sort of mistake less likely. Even with the bug fixed, though, the code is hard to follow. The computation is easier to grasp if we use a variable to hold the number of days in February: ? ?
i f (month == FEB) { i n t nday;
?
nday = 28; i f (yearOA == 0) nday = 29; i f (day > nday) l e g a l = FALSE;
? ? ? ? ?
?
1
The code is still wrong-2000 is a leap year, while 1900 and 2100 are not-but this structure is much easier to adapt to make it absolutely right. By the way, if you work on a program you didn't write, preserve the style you find there. When you make a change, don't use your own style even though you prefer it. The program's consistency is more important than your own, because it makes life easier for those who follow.
Use idioms for consistency. Like natural languages, programming languages have idioms, conventional ways that experienced programmers write common pieces of code. A central part of learning any language is developing a familiarity with its idioms.
12
CHAPTER 1
STYLE
One of the most common idioms is the form of a loop. Consider the C, C++, or Java code for stepping through the n elements of an array, for example to initialize them. Someone might write the loop like this: ?
? ?
i=O; while (i = 0 ; ) arrayCi1 = 1 . 0 ;
All of these are correct, but the idiomatic form is like this: f o r (i = 0 ; i < n; i++) array[i] = 1.0; This is not an arbitrary choice. It visits each member of an n-element array indexed from 0 to n-1. It places all the loop control in the f o r itself, runs in increasing order, and uses the very idiomatic ++ operator to update the loop variable. It leaves the index variable at a known value just beyond the last array element. Native speakers recognize it without study and write it correctly without a moment's thought. In C++ or Java, a common variant includes the declaration of the loop variable: f o r ( i n t i = 0 ; i < n; i++) array[i] = 1.0; Here is the standard loop for walking along a list in C: f o r (p = l i s t ; p != NULL; p
=
p->next)
... Again, all the loop control is in the f o r . For an infinite loop, we prefer for (;;I
...
but while (1)
...
is also popular. Don't use anything other than these forms. Indentation should be idiomatic, too. This unusual vertical layout detracts from readability; it looks like three statements, not a loop:
SECTION 1.3
? ? ? ?
C
? ?
13
for( ap = a r r ; ap < a r r + 128; *ap++ = 0
? ?
CONSISTENCY AND IDIOMS
1
.
1
A standard loop is much easier to read: f o r (ap = a r r ; ap < arr+128; ap++) *ap = 0 ;
Sprawling layouts also force code onto multiple screens or pages, and thus detract from readability. Another common idiom is to nest an assignment inside a loop condition, as in w h i l e ((c = getchar()) putchar(c);
!= EOF)
The do-whi 1e statement is used much less often than f o r and while, because it always executes at least once, testing at the bottom of the loop instead of the top. In many cases, that behavior is a bug waiting to bite, as in this rewrite of the getchar loop: ?
do {
?
c = getchar() ; putchar(c); ) w h i l e (c != EOF);
7
?
It writes a spurious output character because the test occurs after the call to putchar. The do-while loop is the right one only when the body of the loop must always be executed at least once; we'll see some examples later. One advantage of the consistent use of idioms is that it draws attention to nonstandard loops, a frequent sign of trouble: ?
i n t i , t i A r r a y , nmemb;
? ?
? ?
i A r r a y = malloc(nmemb t s i z e o f ( i n t ) ) ; f o r (i = O ; i ='O')&&(cc='9'))?1:0
0
1.5 Magic Numbers Magic tiumbers are the constants, array sizes, character positions, conversion factors, and other literal numeric values that appear in programs.
Give names to magic numbers. As a guideline, any number other than 0 or 1 is likely to be magic and should have a name of its own. A raw number in program source gives no indication of its importance or derivation, making the program harder to understand and modify. This excerpt from a program to print a histogram of letter frequencies on a 24 by 80 cursor-addressed terminal is needlessly opaque because of a host of magic numbers: f a c = l i m / 20; i f (fac c 1) fac = 1;
/a
s e t scale f a c t o r
*/
/ w generate histogram f o r (i = 0 , col = 0 ; i < 27; i + + , j++) { col += 3; k = 2 1 - ( l e t r i ] / fac); s t a r = ( l e t [ i l == 0) ? ' ' : ' * ' ; f o r ( j = k ; j < 22; j++) draw(j, c o l , s t a r ) ;
I
draw(23, 2 , ' ' ) ; /* l a b e l x a x i s f o r (i = ' A ' ; i = ' ') i++; date[il = 0;
/*
One improvement is to rewrite the code more idiomatically:
*/
26 ? ?
? ?
CHAPTER I
STYLE
time(&now) ; strcpy(date, ctime(&now)) ; / a g e t r i d o f t r a i l i n g newline character copied from ctime f o r (i = 0 ; d a t e [ i ] != ' \ n l ; i + + )
*/
? ?
date[i]='\O';
Code and comment now agree, but both can be improved by being made more direct. The problem is to delete the newline that ctime puts on the end of the string it returns. The comment should say so, and the code should do so: time(&now) ; strcpy(date, ctime(&now)) ; /n ctime() puts newline a t end o f s t r i n g ; d e l e t e i t date[strlen(date)-l] = '\0' ;
*/
This last expression is the C idiom for removing the last character from a string. The code is now short, idiomatic, and clear, and the comment supports it by explaining why it needs to be there.
Clarify, don't confuse. Comments are supposed to help readers over the hard parts, not create more obstacles. This example follows our guidelines of commenting the function and explaining unusual properties; on the other hand, the function is strcmp and the unusual properties are peripheral to the job at hand, which is the implementation of a standard and familiar interface: i n t strcmp(char n s l , char ns2) /* s t r i n g comparison r o u t i n e r e t u r n s -1 i f s l i s n/ /* above s2 i n an ascending order l i s t , 0 i f equal a/ /a 1 i f s l below s2 */
C whi 1 e(nsl==as2) if(*sl=='\O') sl++; s2++;
{
return(0);
I i f (nsl>*s2) return(1) ; return(- 1) ;
I When it takes more than a few words to explain what's happening, it's often an indication that the code should be rewritten. Here, the code could perhaps be improved but the real problem is the comment, which is nearly as long as the implementation and confusing, too (which way is "above"?). We're stretching the point to say this routine is hard to understand, but since it implements a standard function, its comment can help by summarizing the behavior and telling us where the definition originates; that's all that's needed:
WHY BOTHER?
SECTION 1.7
27
strcmp: r e t u r n < 0 i f sl<s2, > 0 i f ~ 1 x 2 0 , i f equal n/ ANSI C, s e c t i o n 4.11.4.2 a/ i n t strcmp(const char n s l , const char as2) /a
/*
C
...
I Students are taught that it's important to comment everything. Professional programmers are often required to comment all their code. But the purpose of commenting can be lost in blindly following rules. Comments are meant to help a reader understand pans of the program that are not readily understood from the code itself. As much as possible, write code that is easy to understand; the better you do this, the fewer comments you need. Good code needs fewer comments than bad code.
Exercise 1-11. Comment on these comments. v o i d d i c t : : i n s e r t ( s t r i n g & w) // r e t u r n s 1 i f w i n d i c t i o n a r y , otherwise r e t u r n s 0
i f (n > MAX
II n
% 2 > 0)
// t e s t f o r even number
// W r i t e a message // Add t o l i n e counter f o r each l i n e w r i t t e n v o i d w r i te-message0
C
// increment l i n e counter line-number = line-number + 1; f p r i n t f ( f o u t , "%d %s\n%d %s\n%d %s\n", line-number, HEADER, line-number + 1, BODY, line-number + 2, TRAILER); // increment 1ine counter 1ine-number = 1ine-number + 2;
1
1.7 Why Bother? In this chapter, we've talked about the main concerns of programming style: descriptive names, clarity in expressions, straightforward control flow, readability of code and comments. and the importance of consistent use of conventions and idioms in achieving all of these. It's hard to argue that these are bad things.
28
STYLE
CHAPTER I
But why worry about style? Who cares what a program looks like if it works? Doesn't it take too much time to make it look pretty? Aren't the rules arbitrary anyway? The answer is that well-written code is easier to read and to understand, almost surely has fewer errors, and is likely to be smaller than code that has been carelessly tossed together and never polished. In the rush to get programs out the door to meet some deadline, it's easy to push style aside, to worry about it later. This can be a costly decision. Some of the examples in this chapter show what can go wrong if there isn't enough attention to good style. Sloppy code is bad code-not just awkward and hard to read, but often broken. The key observation is that good style should be a matter of habit. If you think about style as you write code originally, and if you take the time to revise and improve it, you will develop good habits. Once they become automatic, your subconscious will take care of many of the details for you, and even the code you produce under pressure will be better.
Supplementary Reading As we said at the beginning of the chapter, writing good code has much in common with writing good English. Strunk and White's The Elements of Style (Allyn & Bacon) is still the best short book on how to write English well. This chapter draws on the approach of The Elements of Programming Style by Brian Kernighan and P. J. Plauger (McGraw-Hill, 1978). Steve Maguire's Writing Solid Code (Microsoft Press. 1993) is an excellent source of programming advice. There are also helpful discussions of style in Steve McConnell's Code Complete (Microsoft Press. 1993) and Peter van der Linden's Expert C Programming: Deep C Secrets (Prentice Hall, 1994).
Algorithms and Data Structures In the end, only familiarity with the tools and techniques of the field will provide the right solution for a particular problem, and only a certain amount of experience will provide consistently professional results.
Raymond Fielding. The Technique of Special Effects Cinematography The study of algorithms and data structures is one of the foundations of computer science, a rich field of elegant techniques and sophisticated mathematical analyses. And it's more than just fun and games for the theoretically inclined: a good algorithm or data structure might make it possible to solve a problem in seconds that could otherwise take years. In specialized areas like graphics, databases, parsing, numerical analysis, and simulation, the ability to solve problems depends critically on state-of-the-art algorithms and data structures. If you are developing programs in a field that's new to you, you must find out what is already known, lest you waste your time doing poorly what others have already done well. Every program depends on algorithms and data structures, but few programs depend on the invention of brand new ones. Even within an intricate program like a compiler or a web browser, most of the data structures are arrays, lists, trees, and hash tables. When a program needs something more elaborate, it will likely be based on these simpler ones. Accordingly, for most programmers. the task is to know what appropriate algorithms and data structures are available and to understand how to choose among alternatives. Here is the story in a nutshell. There are only a handful of basic algorithms that show up in almost every program-primarily searching and sorting-and even those are often included in libraries. Similarly, almost every data structure is derived from a few fundamental ones. Thus the material covered in this chapter will be familiar to almost all programmers. We have written working versions to make the discussion
30
ALGORITHMS AND DATA STRUCTURES
CHAPTER P
concrete, and you can lift code verbatim if necessary, but do so only after you have investigated what the progamming language and its libraries have to offer.
2.1 Searching Nothing beats an array for storing static tabular data. Compile-time initialization makes it cheap and easy to construct such arrays. (In Java, the initialization occurs at run-time, but this is an unimportant implementation detail unless the arrays are large.) In a progam to detect words that are used rather too much in bad prose, we can write char * f l a b [ ] = " a c t u a l 1 y" , "just", "qui t e n , "really".
NULL
I; The search routine needs to know how many elements are in the array. One way to tell it is to pass the length as an argument; another, used here, is to place a NULL marker at the end of the array: lookup: sequential search f o r word i n a r r a y a/ i n t lookup(char +word, char * a r r a y [ ] )
/a
C int i ; f o r (i = 0 ; a r r a y [ i ] i f (strcrnp(word, return i; r e t u r n -1;
!= NULL; i++) a r r a y [ i ] ) == 0)
I In C and C++, a parameter that is an array of strings can be declared as char * a r r a y [ ] or char *+array. Although these forms are equivalent, the first makes it clearer how the parameter will be used. This search algorithm is called sequential search because it looks at each element in turn to see if it's the desired one. When the amount of data is small, sequential search is fast enough. There are standard library routines to do sequential search for specific data types; for example, functions like s t r c h r and s t r s t r search for the first instance of a given character or substring in a C or C++ string. the Java S t r i n g class has an indexof method. and the generic C++ f i n d algorithms apply to most data types. If such a function exists for the data type you've got, use it. Sequential search is easy but the amount of work is directly proportional to the amount of data to be searched; doubling the number of elements will double the time to search if the desired item is not present. This is a linear relationship-run-time is a linear function of data size-so this method is also known as linear search.
SECTION 2.1
SEARCHING
31
Here's an excerpt from an array of more realistic size from a program that parses HTML, which defines textual names for well over a hundred individual characters: typedef s t r u c t Nameval Nameval ; s t r u c t Nameval C char *name; in t value;
I; /* /a
HTML characters, e. g. AEl i g i s 1i g a t u r e o f A and E. Values are Unicode/IS010646 encoding. */
*/
Nameval html chars [I= C "AE1 ig" , 0x00~6, "Aacute", 0x00~1, "Aci r c " , 0x00~2,
/* ... */ "zeta",
Ox03b6,
1; For a lager array like this, it's more efficient to use binary search. The binary search algorithm is an orderly version of the way we look up words in a dictionary. Check the middle element. If that value is bigger than what we are looking for, look in the first half; otherwise, look in the second half. Repeat until the desired item is found or determined not to be present. For binary search, the table must be sorted, as it is here (that's good style anyway; people find things faster in sorted tables too), and we must know how long the table is. The NELEMS macro from Chapter I can help: p r i n t f ("The HTML tab1e has %d words\nW, NELEMS(htm1chars)) ;
A binary search function for this table might look like this:
/* lookup: binary search f o r name i n tab; r e t u r n index in t lookup(char *name. Nameval tab[], in t ntab)
C i n t low, high, mid, cmp; low = 0; high = ntab - 1; w h i l e (low 0) low = mid + 1; else /a found match */ r e t u r n mid;
1 r e t u r n -1;
I
/*
no match
*/
*/
32
ALGORITHMS AND DATA STRUCTURES
CHAPTER P
Putting all this together. to search html chars we write h a l f = lookup("f r a c l 2 " . htmlchars, NELEMS(htm1chars)) ;
to find the array index of the character %. Binary search eliminates half the data at each step. The number of steps is therefore proportional to the number of times we can divide n by 2 before we're left with a single element. Ignoring roundoff, this is logzn. If we have 1000 items to search, linear search takes up to 1000 steps, while binary search takes about 10; if we have a million items. linear takes a million steps and binary takes 20. The more items, the greater the advantage of binary search. Beyond some size of input (which varies with the implementation), binary search is faster than linear search.
2.2 Sorting Binary search works only if the elements are sorted. If repeated searches are going to be made in some data set, it will be profitable to sort once and then use binary search. If the data set is known in advance, it can be sorted when the program is written and built using compile-time initialization. If not, it must be sorted when the program is run. One of the best all-round sorting algorithms is quicksort, which was invented in 1960 by C. A. R. Hoare. Quicksort is a fine example of how to avoid extra computing. It works by partitioning an array into little and big elements: pick one element of the array (the "pivot"). partition the other elements into two groups: "little ones" that are less than the pivot value, and "big ones" that are greater than or equal to the pivot value. recursively sort each group. When this process is finished, the array is in order. Quicksort is fast because once an element is known to be less than the pivot value, we don't have to compare it to any of the big ones; similarly. big ones are not compared to little ones. This makes it much faster than the simple sorting methods such as insertion sort and bubble sort that compare each element directly to all the others. Quicksort is practical and efficient; it has been extensively studied and myriad variations exist. The version that we present here is just about the simplest implementation but it is certainly not the quickest. This quicksort function sorts an array of integers:
SECTION 2.2
SORTING
/ a quicksort: s o r t v[O]. .v[n-11 i n t o increasing order v o i d q u i c k s o r t ( i n t v[], i n t n)
33
a/
r
i n t i,l a s t ; i f (n = vCO1, the algorithm may swap v [ i ] with itself; this wastes some time but not enough to worry about. P
t t 0
>= p
< P
1
unexamined
t
t
last
i
n-1
34
ALGORITHMS AND DATA STRUCTURES
CHAPTER 2
After all elements have been partitioned, element 0 is swapped with the l a s t element to put the pivot element in its final position; this maintains the correct ordering. Now the array looks like this:
0
last
n-1
The same process is applied to the left and right sub-arrays; when this has finished, the whole array has been sorted. How fast is quicksort? In the best possible case, the first pass partitions n elements into two groups of about n/2 each. the second level partitions two groups, each of about n/2 elements, into four groups each of about n/4. the next level partitions four groups of about n/4 into eight of about n/8. and so on. This goes on for about log, n levels, so the total amount of work in the best case is proportional to n + 2xn/2 + 4xn/4 + 8xn/8 ... (log2n terms), which is nlog2n. On the average, it does only a little more work. It is customary to use base 2 logarithms; thus we say that quicksort takes time proportional to nlogn. This implementation of quicksort is the clearest for exposition, but it has a weakness. If each choice of pivot splits the element values into two nearly equal groups. our analysis is correct, but if the split is uneven too often, the run-time can grow more like n 2 . Our implementation uses a random element as the pivot to reduce the chance that unusual input data will cause too many uneven splits. But if all the input values are the same, our implementation splits off only one element each time and will thus run in time proportional to n '. The behavior of some algorithms depends strongly on the input data. Perverse or unlucky inputs may cause an otherwise well-behaved algorithm to run extremely slowly or use a lot of memory. In the case of quicksort, although a simple implementation like ours might sometimes run slowly, more sophisticated implementations can reduce the chance of pathological behavior to almost zero.
2.3 Libraries The standard libraries for C and C t e include sort functions that should be robust against adverse inputs, and tuned to run as fast as possible. Library routines are prepared to son any data type, but in return we must adapt to their interface, which may be somewhat more complicated than what we showed above. In C, the library function is named q s o r t , and we need to provide a comparison function to be called by q s o r t whenever it needs to compare two values. Since
SECTION 2.3
LIBRARIES
35
the values might be of any type, the comparison function is handed two voi da pointers to the data items to be compared. The function casts the pointers to the proper type, extracts the data values, compares them, and returns the result (negative, zero, or positive according to whether the first value is less than, equal to, or greater than the second). Here's an implementation for sorting an array of strings, which is a common case. We define a function scmp to cast the arguments and call strcmp to do the comparison.
/* scmp: s t r i n g compare o f * p l and ap2 a/ i n t scmp(const void a p l , const void *pi!) i char + v l , av2; v l = *(char a * ) p l ; v2 = *(char a*) p2; r e t u r n strcmp(v1, v2) ;
3 We could write this as a one-line function, but the temporary variables make the code easier to read. We can't use strcmp directly as the comparison function because qsort passes the address of each entry in the array, & s t r [i](of type charaa), not s t r [i](of type char*), as shown in this figure: array of N pointers:
Fp? array
To sort elements str[O] through s t r [ N - l ] of an array of strings, qsort must be called with the array, its length. the size of the items being sorted, and the comparison function: char astr[N] ; q s o r t ( s t r . N. sizeof(str[O]) , scmp);
Here's a similar function i cmp for comparing integers:
CHAPTER 2
/a icmp: i n t e g e r compare o f a p l and ap2 i n t icmp(const v o i d * p l , const v o i d *pi!)
a/
I i n t v l , v2; v l = a ( i n t a) p l ; v2 = * ( i n t a) p2; i f ( v l < v2) r e t u r n -1; e l s e i f ( v l == v2) r e t u r n 0; e l se r e t u r n 1;
3 We could write ?
return vl-v2;
but if v2 is large and positive and v l is large and negative or vice versa, the resulting overflow would produce an incorrect answer. Direct comparison is longer but safe. Again, the call to q s o r t requires the array, its length, the size of the items being sorted, and the comparison function: i n t arr[N]; q s o r t ( a r r , N, sizeof(arr[O]),
icmp);
ANSI C also defines a binary search routine, bsearch. Like qsort, bsearch requires a pointer to a comparison function (often the same one used for qsort); it returns a pointer to the matching element or NULL if not found. Here is our HTML lookup routine, rewritten to use bsearch: /a lookup: use bsearch t o f i n d name i n tab, r e t u r n index i n t lookup(char *name, Nameval tab[], i n t ntab)
*/
C Nameval key, anp; key.name = name; key-value = 0; /a unused; anything w i l l do np = (Nameval a) bsearch(&key, tab, ntab, s i z e o f (tablo]), nvcmp) ; i f (np == NULL) r e t u r n -1; else r e t u r n np-tab;
a/
3 As with qsort, the comparison routine receives the address of the items to be compared, so the key must have that type; in this example, we need to construct a fake Nameval entry that is passed to the comparison routine. The comparison routine itself
SECTION 2.4
A JAVA QUICKSORT
37
is a function nvcmp that compares two Nameval items by calling strcmp on their string components, ignoring their values:
/* nvcmp: compare two Nameval names */ i n t nvcmp(const void ava, const void avb) const Nameval *a, ab; a = (Nameval a) va; b = (Nameval a) vb: r e t u r n strcmp(a->name,
b->name);
3 This is analogous to scmp but differs because the strings are stored as members of a structure. The clumsiness of providing the key means that bsearch provides less leverage than qsort. A good general-purpose sort routine takes a page or two of code, while binary search is not much longer than the code it takes to interface to bsearch. Nevertheless, it's a good idea to use bsearch instead of writing your own. Over the years, binary search has proven surprisingly hard for programmers to get right. The standard C++ library has a generic algorithm called s o r t that guarantees O(n1ogn) behavior. The code is easier because it needs no casts or element sizes. and it does not require an explicit comparison function for types that have an order relation. i n t arrCN1;
The C++ library also has generic binary search routines, with similar notational advantages.
Exercise 2-1. Quicksort is most naturally expressed recursively. Write it iteratively and compare the two versions. (Hoare describes how hard it was to work out quicksort iteratively, and how neatly it fell into place when he did it recursively.)
2.4 A Java Quicksort The situation in Java is different. Early releases had no standard sort function, so we needed to write our own. More recent versions do provide a s o r t function. however, which operates on classes that implement the Comparable interface, so we can now ask the library to sort for us. But since the techniques are useful in other situations, in this section we will work through the details of implementing quicksort in Java.
38
ALGORITHMS AND DATA STRUCTURES
CHAPTER P
It's easy to adapt a quicksort for each type we might want to sort. but it is more instructive to write a generic sort that can be called for any kind of object. more in the style of the q s o r t interface. One big difference from C or C u is that in Java it is not possible to pass a comparison function to another function; there are no function pointers. Instead we create an interjGace whose sole content is a function that compares two Objects. For each data type to be sorted, we then create a class with a member function that implements the interface for that data type. We pass an instance of that class to the sort function, which in turn uses the comparison function within the class to compare elements. We begin by defining an interface named Cmp that declares a single member, a comparison function cmp that compares two Objects: i n t e r f a c e Cmp { i n t cmp(0bject x , Object y ) ; Then we can write comparison functions that implement this interface; for example, this class defines a function that compares Integers:
// Icmp : Integer comparison c l a s s Icmp implements Cmp { public i n t cmp(0bject 01, Object 02) C
((Integer) 01).i ntVal ue() ; ((Integer) 02). i ntVal ue() ; i f ( i l < i2) return - 1; e l s e i f ( i l == i 2 ) return else return
in t i1 in t i2
= =
and this compares S t r i ngs:
// Scmp: S t r i n g comparison c l a s s Scmp implements Cmp public i n t cmp(0bject 01. Object 02) C
S t r i n g s l = (String) 01; S t r i n g s2 = (String) 02; return sl.compareTo(s2) ;
1
3 We can sort only types that are derived from Object with this mechanism; it cannot be applied to the basic types like i n t or double. This is why we sort Integers rather than i n ts.
SECTION 2.4
A JAVA QUICKSORT
39
With these components, we can now translate the C quicksort function into Java and have i t call the comparison function from a Cmp object passed i n as an argument. The most significant change is the use of indices 1e f t and r i ght. since Java does not have pointers into arrays.
// Quicksort. s o r t : quicksort v [ l e f t ] . . v [ r i g h t ] s t a t i c void sort(Object[] v, i n t l e f t , i n t r i g h t , Cmp cmp)
C i n t i, l a s t ;
i f ( l e f t >= r i g h t ) // nothing t o do return; swap(v, l e f t , rand(1eft. r i g h t ) ) ; // last = left; // f o r (i= l e f t + l ; i left, f n , arg); applypostorder(treep->right. f n , arg) ; (af n) (treep , arg) ;
1 Post-order traversal is used when the operation on the node depends on the subtrees below it. Examples include computing the height of a tree (take the maximum of the height of each of the two subtrees and add one), laying out a tree in a graphics drawing package (allocate space on the page for each subtree and combine them for this node's space), and measuring total storage. A third choice, pre-order, is rarely used so we'll omit it. Realistically, binary search trees are infrequently used, though B-trees, which have very high branching, are used to maintain information on secondary storage. In dayto-day programming, one common use of a tree is to represent the structure of a statement or expression. For example, the statement mid
=
(low + high) / 2 ;
can be represented by the parse tree shown in the figure below. To evaluate the tree, do a post-order traversal and perform the appropriate operation at each node.
/ \/
mid
/ \high
1 ow
We'll take a longer look at parse trees in Chapter 9.
Exercise 2-11. Compare the performance of 1 ookup and nrl ookup. How expensive is recursion compared to iteration? Exercise 2-12. Use in-order traversal to create a sort routine. What time complexity does it have? Under what conditions might it behave poorly? How does its performance compare to our quicksort and a library version? Exercise 2-13. Devise and implement a set of tests for verifying that the tree routines are correct.
SECTION 2.9
HASH TABLES
55
2.9 Hash Tables Hash tables are one of the great inventions of computer science. They combine arrays, lists, and some mathematics to create an efficient structure for storing and retrieving dynamic data. The typical application is a symbol table. which associates some value (the data) with each member of a dynamic set of strings (the keys). Your favorite compiler almost certainly uses a hash table to manage information about each variable in your program. Your web browser may well use a hash table to keep track of recently-used pages, and your connection to the Internet probably uses one to cache recently-used domain names and their IP addresses. The idea is to pass the key through a hash function to generate a hash value that will be evenly distributed through a modest-sized integer range. The hash value is used to index a table where the information is stored. Java provides a standard interface to hash tables. In C and C++ the usual style is to associate with each hash value (or "bucket") a list of the items that share that hash, as this figure illustrates: symtabCNHASH1:
hash chains:
-
NULL NULL NULL
name 1
-
value 1
NULL
name 2
value 2
NULL
NULL NULL
name 3 value 3
In practice, the hash function is pre-defined and an appropriate size of array is allocated, often at compile time. Each element of the array is a list that chains together the items that share a hash value. In other words, a hash table of n items is an array of lists whose average length is n/(.array size). Retrieving an item is an O(.1) operation provided we pick a good hash function and the lists don't grow too long. Because a hash table is an array of lists, the element type is the same as for a list: typedef s t r u c t Nameval Nameval ; s t r u c t Nameval { char *name; i nt val ue ; Nameval *next; /t i n chain */
I; Nameval tsymtab [NHASH] ; /* a symbol tab1 e */ The list techniques we discussed in Section 2.7 can be used to maintain the individual hash chains. Once you've got a good hash function, it's smooth sailing: just pick the hash bucket and walk along the list looking for a perfect match. Here is the code for a
56
ALGORITHMS AND DATA STRUCTURES
CHAPTER P
hash table lookuplinsert routine. If the item is found, it is returned. If the item is not found and the c r e a t e flag is set, lookup adds the item to the table. Again, this does not create a copy of the name, assuming that the caller has made a safe copy instead. / t lookup: f i n d name i n symtab, w i t h optional c r e a t e Nameval* lookup(char tname, i n t c r e a t e , i n t value)
t/
C i n t h; Nameval usym; h = hashcname) ; f o r (sym = symtab[h]; sym != NULL; sym = sym->next) i f (strcmp(name, sym->name) == 0) r e t u r n sym; i f (create) { sym = (Nameval t) emall oc ( s i zeof (Nameval ) ) ; sym->name = name; /t assumed a l l o c a t e d elsewhere sym->value = value; sym->next = symtab[h]; symtab[h] = sym;
t/
1 r e t u r n sym;
1 This combination of lookup and optional insertion is common. Without it, there is duplication of effort; one must write i f (lookup("namel') == NULL) addi tem(newi tem("name" , value)) ;
and the hash is computed twice. How big should the array be? The general idea is to make it big enough that each hash chain will have at most a few elements, so that lookup will be O(1). For instance, a compiler might have an array size of a few thousand, since a large source file has a few thousand lines, and we don't expect more than about one new identifier per line of code. We must now decide what the hash function, hash, should calculate. The function must be deterministic and should be fast and distribute the data uniformly throughout the array. One of the most common hashing algorithms for strings builds a hash value by adding each byte of the string to a multiple of the hash so far. The multiplication spreads bits from the new byte through the value so far; at the end of the loop, the result should be a thorough mixing of the input bytes. Empirically, the values 31 and 37 have proven to be good choices for the multiplier in a hash function for ASCII strings. enum { MULTIPLIER = 3 1 };
SECTION 2.9
HASH TABLES
/ t hash: compute hash value o f s t r i n g unsigned i n t hash(char t s t r ) { unsigned i n t h; unsigned char t p :
57
t/
h = 0: for (p = (unsigned char a) s t r ; *p != '\O1; p++) h = MULTIPLIER * h + *p; r e t u r n h % NHASH;
1 The calculation uses unsigned characters because whether char is signed is not specified by C and C++, and we want the hash value to remain positive. The hash function returns the result modulo the size of the array. If the hash function distributes key values uniformly, the precise array size doesn't matter. It's hard to be certain that a hash function is dependable, though, and even the best function may have trouble with some input sets, so it's wise to make the array size a prime number to give a bit of extra insurance by guaranteeing that the array size, the hash multiplier, and likely data values have no common factor. Experiments show that for a wide variety of strings it's hard to construct a hash function that does appreciably better than the one above, but it's easy to make one that does worse. An early release of Java had a hash function for strings that was more efficient if the string was long. The hash function saved time by examining only 8 or 9 characters at regular intervals throughout strings longer than 16 characters. starting at the beginning. Unfortunately, although the hash function was faster, it had bad statistical properties that canceled any performance gain. By skipping pieces of the string, it tended to miss the only distinguishing part. File names begin with long identical prefixes-the directory name-and may differ only in the last few characters (.. j a v a versus .class). URLs usually begin with h t t p : //w.and end with .html, so they tend to differ only in the middle. The hash function would often examine only the non-varying part of the name, resulting in long hash chains that slowed down searching. The problem was resolved by replacing the hash with one equivalent to the one we have shown (with a multiplier of 37), which examines every character of the string. A hash function that's good for one input set (say, short variable names) might be poor for another (URLs), so a potential hash function should be tested on a variety of typical inputs. Does it hash short strings well? Long strings? Equal length strings with minor variations? Strings aren't the only things we can hash. We could hash the three coordinates of a particle in a physical simulation, reducing the storage to a linear table (O(number of particles)) instead of a three-dimensional array (.O(.xsize x ysize x zsize)). One remarkable use of hashing is Gerard Holzmann's Supertrace program for analyzing protocols and concurrent systems. Supertrace takes the full information for each possible state of the system under analysis and hashes the information to generate the address of a single bit in memory. If that bit is on, the state has been seen
58
ALGORITHMS AND DATA STRUCTURES
CHAPTER 2
before; if not, it hasn't. Supertrace uses a hash table many megabytes long, but stores only a single bit in each bucket. There is no chaining; if two states collide by hashing to the same value, the program won't notice. Supertrace depends on the probability of collision being low (it doesn't need to be zero because Supertrace is probabilistic. not exact). The hash function is therefore particularly careful; it uses a cyclic redundancy check, a function that produces a thorough mix of the data. Hash tables are excellent for symbol tables, since they provide expected O(1) access to any element. They do have a few limitations. If the hash function is poor or the table size is too small, the lists can grow long. Since the lists are unsorted, this leads to O ( n )behavior. The elements are not directly accessible in sorted order, but it is easy to count them, allocate an array, fill it with pointers to the elements, and sort that. Still, when used properly, the constant-time lookup, insertion, and deletion properties of a hash table are unmatched by other techniques.
Exercise 2-14. Our hash function is an excellent general-purpose hash for strings. Nonetheless, peculiar data might cause poor behavior. Construct a data set that causes our hash function to perform badly. Is it easier to find a bad set for different values of NHASH? Exercise 2-15. Write a function to access the successive elements of the hash table in unsorted order. Exercise 2-16. Change lookup so that if the average list length becomes more than x, the array is grown automatically by a factor of y and the hash table is rebuilt. Exercise 2-17. Design a hash function for storing the coordinates of points in 2 dimensions. How easily does your function adapt to changes in the type of the coordinates, for example from integer to floating point or from Cartesian to polar coordinates, or to changes from 2 to higher dimensions?
2.10 Summary There are several steps to choosing an algorithm. First, assess potential algorithms and data structures. Consider how much data the program is likely to process. If the problem involves modest amounts of data, choose simple techniques; if the data could grow, eliminate designs that will not scale up to large inputs. Then, use a library or language feature if you can. Failing that, write or borrow a short, simple, easy to understand implementation. Try it. If measurements prove it to be too slow, only then should you upgrade to a more advanced technique. Although there are many data structures, some vital to good performance in special circumstances, most programs are based largely on arrays, lists, trees, and hash tables. Each of these supports a set of primitive operations, usually including: create a
SECTION 2.10
SUMMARY
59
new element, find an element, add an element somewhere, perhaps delete an element, and apply some operation to all elements. Each operation has an expected computation time that often determines how suitable this data type (or implementation) is for a particular application. Arrays support constant-time access to any element but do not grow or shrink gracefully. Lists adjust well to insertions and deletions, but take O ( n )time to access random elements. Trees and hash tables provide a good compromise: rapid access to specific items combined with easy growth, so long as some balance criterion is maintained. There are other more sophisticated data structures for specialized problems, but this basic set is sufficient to build the great majority of software. -
Supplementary Reading Bob Sedgewick's family of Algorithms books (Addison-Wesley) is an excellent place to find accessible treatments of a variety of useful algorithms. The third edition of Algorithms in C + + (1998) has a good discussion of hash functions and table sizes. Don Knuth's The Art of Computer Programming (.Addison-Wesley) is the definitive source for rigorous analyses of many algorithms; Volume 3 (2nd Edition, 1998) covers sorting and searching. Supertrace is described in Design and Validation of Computer Protocols by Gerard Holzmann (Prentice Hall. 1991). Jon Bentley and Doug McIlroy describe the creation of a fast and robust quicksort in "Engineering a sort function," Software-Practice and Experience, 23, 1, pp. 1249-1265, 1993.
Design and Implementation Show me yourflowcharts and conceal your tables, and I shall continue to be mystijied. Show me your tables, and I won't usually need your flowcharts; they'll be obvious.
Frederick P. Brooks, Jr., The Mythical Man Month As the quotation from Brooks's classic book suggests, the design of the data structures is the central decision in the creation of a program. Once the data structures are laid out, the algorithms tend to fall into place, and the coding is comparatively easy. This point of view is oversimplified but not misleading. In the previous chapter we examined the basic data structures that are the building blocks of most programs. In this chapter we will combine such structures as we work through the design and implementation of a modest-sized program. We will show how the problem influences the data structures, and how the code that follows is straightforward once we have the data structures mapped out. One aspect of this point of view is that the choice of programming language is relatively unimportant to the overall design. We will design the program in the abstract and then write it in C. Java, C++, Awk, and Perl. Comparing the implementations demonstrates how languages can help or hinder, and ways in which they are unimportant. Program design can certainly be colored by a language but is not usually dominated by it. The problem we have chosen is unusual, but in basic form it is typical of many programs: some data comes in, some data goes out, and the processing depends on a little ingenuity. Specifically, we're going to generate random English text that reads well. If we emit random letters or random words, the result will be nonsense. For example, a program that randomly selects letters (and blanks. to separate words) might produce this: xptmxgn xusaja
afqnzgxl
1h i dlwcd rjdjuvpydrlwnjy
62
DESIGN AND IMPLEMENTATION
CHAPTER 3
which is not very convincing. If we weight the letters by their frequency of appearance in English text, we might get this: i d t e f o a e t c s t r d e r j c i i ofdslnqetacp t o l a
which isn't a great deal better. Words chosen from the dictionary at random don't make much more sense: pol ydactyl equatori a1 spl ashi1 y jowl verandah c i rcumscri be
For better results, we need a statistical model with more structure. such as the frequency of appearance of whole phrases. But where can we find such statistics? We could grab a large body of English and study it in detail, but there is an easier and more entertaining approach. The key observation is that we can use any existing text to construct a statistical model of the language as used in that text, and from that generate random text that has similar statistics to the original.
3.1 The Markov Chain Algorithm An elegant way to do this sort of processing is a technique called a Markov chain algorithm. If we imagine the input as a sequence of overlapping phrases, the algorithm divides each phrase into two parts, a multi-word prefix and a single suflx word that follows the prefix. A Markov chain algorithm emits output phrases by randomly choosing the suffix that follows the prefix, according to the statistics of (in our case) the original text. Three-word phrases work well--a two-word prefix is used to select the suffix word: set w I and w2 to the first two words in the text print w ,and w 2 loop: randomly choose w 3 ,one of the successors of prefix w w 2 in the text print w replace w and w ;? by w ;? and w repeat loop
-, ,
To illustrate, suppose we want to generate random text based on a few sentences paraphrased from the epigraph above, using two-word prefixes: Show your flowcharts and conceal your tables and I w i l l be m y s t i f i e d . Show your tables and your flowcharts w i l l be obvious . (end)
These are some of the pairs of input words and the words that follow them:
THE MARKOV CHAIN ALGORITHM
SECTION 3.1
Input prefix:
Suffix words tlzat follow:
Show your your flowcharts flowcharts and flowcharts w i l l your t a b l es w i l l be be m y s t i f i e d . be obvious.
flowcharts t a b l es and w i l l conceal be and and m y s t i f i e d . obvious. Show
63
(endl
A Markov algorithm processing this text will begin by printing Show your and will then randomly pick either flowcharts or tables. If it chooses the former, the current prefix becomes your flowcharts and the next word will be and or wi1l . If it chooses tables, the next word will be and. This continues until enough output has been generated or until the end-marker is encountered as a suffix. Our program will read a piece of English text and use a Markov chain algorithm to generate new text based on the frequency of appearance of phrases of a fixed length. The number of words in the prefix, which is two in our example, is a parameter. Making the prefix shorter tends to produce less coherent prose; making it longer tends to reproduce the input text verbatim. For English text, using two words to select a third is a good compromise; it seems to recreate the flavor of the input while adding its own whimsical touch. What is a word? The obvious answer is a sequence of alphabetic characters, but it is desirable to leave punctuation attached to the words so "words" and "words. " are different. This helps to improve the quality of the generated prose by letting punctuation, and therefore (indirectly) grammar, influence the word choice, although it also permits unbalanced quotes and parentheses to sneak in. We will therefore define a "word" as anything between white space, a decision that places no restriction on input language and leaves punctuation attached to the words. Since most programming languages have facilities to split text into white-space-separated words, this is also easy to implement. Because of the method, all words, all two-word phrases, and all three-word phrases in the output must have appeared in the input, but there should be many fourword and longer phrases that are synthesized. Here are a few sentences produced by the program we will develop in this chapter, when given the text of Chapter VII of The Sun Also Rises by Ernest Hemingway: As I started up the undershirt onto his chest black, and big stomach muscles bulging under the light. "You see them?" Below the line where his ribs stopped were two raised white welts. "See on the forehead." "Oh, Brett, I love you." "Let's not talk. Talking's all bilge. I'm going away tomorrow." "Tomorrow?" "Yes. Didn't I say so? I am." "Let's have a drink, then." We were lucky here that punctuation came out correctly; that need not happen.
CHAPTER 3
3.2 Data Structure Alternatives How much input do we intend to deal with? How fast must the program run? It seems reasonable to ask our program to read in a whole book, so we should be prepared for input sizes of n = 100,000 words or more. The output will be hundreds or perhaps thousands of words, and the program should run in a few seconds instead of minutes. With 100,000 words of input text, n is fairly large so the algorithms can't be too simplistic if we want the program to be fast. The Markov algorithm must see all the input before it can begin to generate output. so it must store the entire input in some form. One possibility is to read the whole input and store it in a long string, but we clearly want the input broken down into words. If we store it as an array of pointers to words, output generation is simple: to produce each word, scan the input text to see what possible suffix words follow the prefix that was just emitted, and then choose one at random. However, that means scanning all 100,000 input words for each word we generate; 1,000 words of output means hundreds of millions of string comparisons. which will not be fast. Another possibility is to store only unique input words, together with a list of where they appear in the input so that we can locate successor words more quickly. We could use a hash table like the one in Chapter 2, but that version doesn't directly address the needs of the Markov algorithm, which must quickly locate all the suffixes of a given prefix. We need a data structure that better represents a prefix and its associated suffixes. The program will have two passes, an input pass that builds the data structure representing the phrases, and an output pass that uses the data structure to generate the random output. In both passes, we need to look up a prefix (quickly): in the input pass to update its suffixes, and in the output pass to select at random from the possible suffixes. This suggests a hash table whose keys are prefixes and whose values are the sets of suffixes for the corresponding prefixes. For purposes of description, we'll assume a two-word prefix, so each output word is based on the pair of words that precede it. The number of words in the prefix doesn't affect the design and the programs should handle any prefix length, but selecting a number makes the discussion concrete. The prefix and the set of all its possible suffixes we'll call a state, which is standard terminology for Markov algorithms. Given a prefix, we need to store all the suffixes that follow it so we can access them later. The suffixes are unordered and added one at a time. We don't know how many there will be, so we need a data structure that grows easily and efficiently. such as a list or a dynamic array. When we are generating output, we need to be able to choose one suffix at random from the set of suffixes associated with a particular prefix. Items are never deleted. What happens if a phrase appears more than once? For example, 'might appear twice' might appear twice but 'might appear once' only once. This could be represented by putting 'twice' twice in the suffix list for 'might appear' or by putting it in once, with an associated counter set to 2. We've tried it with and without counters;
SECTION 3.3
BUILDING THE DATA STRUCTURE IN c
65
without is easier. since adding a suffix doesn't require checking whether it's there already, and experiments showed that the difference in run-time was negligible. In summary, each state comprises a prefix and a list of suffixes. This information is stored in a hash table, with prefix as key. Each prefix is a fixed-size set of words. If a suffix occurs more than once for a given prefix, each occurrence will be included separately in the list. The next decision is how to represent the words themselves. The easy way is to store them as individual strings. Since most text has many words appearing multiple times, it would probably save storage if we kept a second hash table of single words, so the text of each word was stored only once. This would also speed up hashing of prefixes, since we could compare pointers rather than individual characters: unique strings have unique addresses. We'll leave that design as an exercise; for now, strings will be stored individually.
3.3 Building the Data Structure in C Let's begin with a C implementation. The first step is to define some constants. enum I NPREF NHASH MAXGEN
= 2, /* number o f p r e f i x words */ = 4093, /a s i z e o f s t a t e hash t a b l e a r r a y = 10000 /* maximum words generated */
*/
3; This declaration defines the number of words (NPREF) for the prefix, the size of the hash table array (NHASH). and an upper limit on the number of words to generate (MAXGEN). If NPREF is a compile-time constant rather than a run-time variable, storage management is simpler. The array size is set fairly large because we expect to give the program large input documents, perhaps a whole book. We chose NHASH = 4093 so that if the input has 10,000 distinct prefixes (word pairs). the average chain will be very short, two or three prefixes. The larger the size, the shorter the expected length of the chains and thus the faster the lookup. This program is really a toy, so the performance isn't critical, but if we make the array too small the program will not handle our expected input in reasonable time; on the other hand, if we make it too big it might not fit in the available memory. The prefix can be stored as an array of words. The elements of the hash table will be represented as a S t a t e data type, associating the S u f f i x list with the prefix: typedef s t r u c t S t a t e S t a t e ; typedef s t r u c t S u f f i x S u f f i x ; s t r u c t S t a t e { /* p r e f i x + s u f f i x l i s t */ char * p r e f [NPREF] ; /* p r e f i x words */ S u f f i x asuf; /* l i s t o f s u f f i x e s */ State *next; / a next i n hash t a b l e */
3;
66
DESIGN AND IMPLEMENTATION
CHAPTER 3
s t r u c t S u f f i x { /* l i s t o f s u f f i x e s */ char *word; /* s u f f i x */ S u f f i x *next; /a next i n l i s t o f s u f f i x e s a/
1; State
*statetab[NHASH] ;
/*
hash t a b l e o f s t a t e s
*/
Pictorially, the data structures look like this:
statetab:
We need a hash function for prefixes, which are arrays of strings. It is simple to modify the string hash function fmm Chapter 2 to loop over the strings in the array, thus in effect hashing the concatenation of the strings: /a hash: compute hash value f o r array o f NPREF s t r i n g s unsigned i n t hash(char *s [NPREF])
*/
f unsigned in t h; unsigned char *p; i n t i; h = 0; f o r (i= 0; i < NPREF; i++) f o r (p = (unsigned char *) s [i] ; h = MULTIPLIER * h + *p; r e t u r n h % NHASH;
1 A similar modification to the lookup routine completes the implementation of the hash table:
BUILDING THE DATA STRUCTURE IN c
SECTION 3.3
/* lookup: search f o r prefix; create i f requested. */ /* returns pointer i f present or created; NULL i f not. */ /* creation doesn't strdup so strings mustn't change l a t e r .
67
a/
State* lookup(char *prefix[NPREF] , i n t create)
1
i n t i , h; State *sp; h = hash(prefix); f o r (sp = statetab[h]; sp != NULL; sp = sp->next) f o r (i = 0; i < NPREF; i++) i f (strcmp(prefix[i] , sp-bpref [i]) != 0) break; i f (i == NPREF) /* found i t a/ return sp;
1
i f (create) ( sp = (State *) emall oc(si zeof (State)) ; f o r (i = 0 ; i < NPREF; i++) sp->pref [i] = prefix[i] ; sp->suf = NULL; sp->next = statetab[h] ; statetab[hl = sp;
1
return sp;
1 Notice that 1ookup doesn't make a copy of the incoming strings when it creates a new state; it just stores pointers in sp-bpref [I. Callers of lookup must guarantee that the data won't be overwritten later. For example, if the strings are in an I/0 buffer, a copy must be made before 1ookup is called; otherwise, subsequent input could overwrite the data that the hash table points to. Decisions about who owns a resource shared across an interface arise often. We will explore this topic at length in the next chapter. Next we need to build the hash table as the file is read:
/* build: read input, build prefix table void build(char *prefix[NPREF] , F I L E *f) 1
a/
char buf [100], fmt [lo] ; /a create a format s t r i n g ; %s could overflow buf */ sprintf (fmt, "%%%dsn,sizeof (buf)-1) ; while (fscanfcf, fmt, buf) != EOF) add(prefi x, estrdupcbuf)) :
1 The peculiar call to sprintf gets around an irritating problem with fscanf, which is otherwise perfect for the job. A call to fscanf with format %s will read the next white-space-delimited word from the file into the buffer, but there is no limit on size: a long word might overflow the input buffer, wreaking havoc. If the buffer is 100
68
DESIGN AND IMPLEMENTATION
CHAPTER 3
bytes long (which is far beyond what we expect ever to appear in normal text), we can use the format 9699s (leaving one byte for the terminal '\O'), which tells fscanf to stop after 99 bytes. A long word will be broken into pieces, which is unfortunate but safe. We could declare ? ?
enum { BUFSIZE = 100 ); char fmt[] = "%99s"; /* BUFSIZE- 1 */
but that requires two constants for one arbitrary decision-the size of the buffer-and introduces the need to maintain their relationship. The problem can be solved once and for all by creating the format string dynamically with sprintf, so that's the approach we take. The two arguments to build are the prefix array holding the previous NPREF words of input and a F I L E pointer. It passes the prefix and a copy of the input word to add, which adds the new entry to the hash table and advances the prefix:
/* add: add word t o suffix l i s t , update prefix */ void add(char *prefix[NPREF] , char *suffix)
I
State *sp; sp = lookup(prefix, 1); /* create i f not found */ addsuffix(sp, suffix); /* move the words down the prefix a/ memmove(prefix, prefix+l. (NPREF-l)*sizeof (prefix[O])) ; prefixCNPREF-11 = suffix;
1 The call to memmove is the idiom for deleting from an array. It shifts elements 1 through NPREF-1 in the prefix down to positions 0 through NPREF-2, deleting the first prefix word and opening a space for a new one at the end. The addsuff i x routine adds the new suffix:
/* addsuffix: add t o s t a t e . suffix must not change l a t e r a/ void addsuffix(State asp, char *suffix)
C
Suffix *suf; suf = (Suffix *) emalloc(sizeof (Suffix)) ; suf->word = suffix; suf->next = sp->suf; sp->suf = suf;
1 We split the action of updating the state into two functions: add performs the general service of adding a suffix to a prefix, while addsuffix performs the implementationspecific action of adding a word to a suffix list. The add routine is used by bui 1 d. but addsuffix is used internally only by add; it is an implementation detail that might change and it seems better to have it in a separate function. even though it is called in only one place.
GENERATING OUTPUT
SECTION 3.4
69
3.4 Generating Output With the data structure built, the next step is to generate the output. The basic idea is as before: given a prefix, select one of its suffixes at random, print it, then advance the prefix. This is the steady state of processing; we must still figure out how to start and stop the algorithm. Starting is easy if we remember the words of the first prefix and begin with them. Stopping is easy, too. We need a marker word to terminate the algorithm. After all the regular input, we can add a terminator. a "word" that is guaranteed not to appear in any input:
.
build(prefix, stdin) ; add (pref i x NONWORD) ; NONWORD should be some value that will never be encountered in regular input. Since
the input words are delimited by white space, a "word" of white space will serve, such as a newline character: char NONWORD[]
=
"\n";
/* cannot appear as real word */
One more wony: what happens if there is insufficient input to start the algorithm? There are two approaches to this sort of problem, either exit prematurely if there is insufficient input, or arrange that there is always enough and don't bother to check. In this program, the latter approach works well. We can initialize building and generating with a fabricated prefix, which guarantees there is always enough input for the program. To prime the loops, initialize the prefix array to be all NONWORD words. This has the nice benefit that the first word of the input file will be the first suflx of the fake prefix, so the generation loop needs to print only the suffixes it produces. In case the output is unmanageably long, we can terminate the algorithm after some number of words are produced or when we hit NONWORD as a suffix, whichever comes first. Adding a few NONWORDs to the ends of the data simplifies the main processing loops of the program significantly; it is an example of the technique of adding sentinel values to mark boundaries. As a rule, try to handle irregularities and exceptions and special cases in data. Code is harder to get right so the control flow should be as simple and regular as possible. The generate function uses the algorithm we sketched originally. It produces one word per line of output, which can be grouped into longer lines with a word processor; Chapter 9 shows a simple formatter called fmt for this task. With the use o i the initial and final NONWORD strings, generate starts and stops proper1y:
CHAPTER 3
/* generate: produce output, one word per l i n e */ void generateci n t nwords) {
S t a t e .asp; Suffix *suf; char *prefix[NPREF] , *w; i n t i , nmatch; f o r (i = 0; i < NPREF; i++) /* reset i n i t i a l prefix */ prefix [i ] = NONWORD ; f o r (i = 0; i < nwords; i++) { sp = lookup(prefix, 0) ; nmatch = 0; f o r (suf = sp->suf; suf != NULL; suf = suf->next) i f (rand() % ++match == 0) /* prob = l/nmatch */ w = suf->word; i f (strcmp(w. NONWORD) == 0) break; , w) ; printf ("%s\nW memmove(prefix, prefix+l, (NPREF-l)*sizeof(prefix[O])); prefix[NPREF-l] = w;
1 Notice the algorithm for selecting one item at random when we don't know how many items there are. The variable nmatch counts the number of matches as the list is scanned. The expression
increments nmatch and is then true with probability l/nmatch. Thus the first matching item is selected with probability 1. the second will replace it with probability 112. the third will replace the survivor with probability 113, and so on. At any time, each one of the k matching items seen so far has been selected with probability l/k. At the beginning. we set the prefix to the starting value, which is guaranteed to be installed in the hash table. The first Suffix values we find will be the first words of the document. since they are the unique follow-on to the starting prefix. After that, random suffixes will be chosen. The loop calls lookup to find the hash table entry for the current prefix. then chooses a random suffix, prints it, and advances the prefix. If the suffix we choose is NONWORD, we're done, because we have chosen the state that corresponds to the end of the input. If the suffix is not NONWORD, we print it, then drop the first word of the prefix with a call to memmove, promote the suffix to be the last word of the prefix, and loop. Now we can put all this together into a main routine that reads the standard input and generates at most a specified number of words:
SECTION 3.5
JAVA
71
/* markov main: markov-chain random t e x t generation */ i n t mai n (voi d) {
i n t i , nwords = MAXGEN ; char *prefix[NPREF] ; / a current input prefix a/ f o r (i = 0; i < NPREF; i++) /* s e t up i n i t i a l prefix */ pref i x[i] = NONWORD; buildcprefix, stdin); add (prefi x, NONWORD) ; generate(nw0rds); return 0 ;
1 This completes our C implementation. We will return at the end of the chapter to a comparison of programs in different languages. The great strengths of C are that it gives the programmer complete control over implementation, and programs written in it tend to be fast. The cost, however, is that the C programmer must do more of the work, allocating and reclaiming memory, creating hash tables and linked lists, and the like. C is a razor-sharp tool, with which one can create an elegant and efficient program or a bloody mess. Exercise 3-1. The algorithm for selecting a random item from a list of unknown length depends on having a good random number generator. Design and carry out experiments to determine how well the method works in practice. Exercise 3-2. If each input word is stored in a second hash table, the text is only stored once, which should save space. Measure some documents to estimate how much. This organization would allow us to compare pointers rather than strings in the hash chains for prefixes, which should run faster. lmplement this version and measure the change in speed and memory consumption. Exercise 3-3. Remove the statements that place sentinel NONWORDs at the beginning and end of the data, and modify generate so it starts and stops properly without them. Make sure it produces correct output for input with 0, 1, 2, 3, and 4 words. Compare this implementation to the version using sentinels.
3.5 Java Our second implementation of the Markov chain algorithm is in Java. Objcctoriented languages like Java encourage one to pay particular attention to the interfaces between the components of the program. which are then encapsulated as independent data items called objects or classes, with associated functions called methods. Java has a richer library than C, including a set of contuiner classes to group existing objects in various ways. One example is a Vector that provides a dynamicallygrowable array that can store any Object type. Another example is the Hashtable
72
DESIGN AND IMPLEMENTATION
CHAPTER 3
class, with which one can store and retrieve values of one type using objects of another type as keys. In our application, Vectors of strings are the natural choice to hold prefixes and suffixes. We can use a Hashtable whose keys are prefix vectors and whose values are suffix vectors. The terminology for this type of construction is a map from prefixes to suffixes; in Java, we need no explicit State type because Hashtable implicitly connects (maps) prefixes to suffixes. This design is different from the C version, in which we installed State structures that held both prefix and suffix list, and hashed on the prefix to recover the full State. A Hashtabl e provides a p u t method to store a key-value pair, and a get method to retrieve the value for a key: Hashtable h = new Hashtable(); h.put(key, value); Sometype v = (Sometype) h.get(key);
Our implementation has three classes. The first class. P r e fix, holds the words of the prefix: class P r e f i x { p u b l i c Vector p r e f ;
// NPREF adjacent words from i n p u t
The second class, Chain, reads the input, builds the hash table, and generates the output; here are its class variables: class Chain { s t a t i c f i n a l i n t NPREF = 2; // s i z e o f p r e f i x s t a t i c f i n a l S t r i n g NONWORD = "\nW; // "word" t h a t c a n ' t appear Hashtable s t a t e t a b = new Hashtable() ; // key = P r e f i x , value = s u f f i x Vector P r e f i x p r e f i x = new Prefix(NPREF, NONWORD) ; // i n i t i a l p r e f i x Random rand = new Random();
...
The third class is the public interface; it holds main and instantiates a Chain: class Markov I s t a t i c f i n a l i n t MAXCEN = 10000; // maximum words generated p u b l i c s t a t i c v o i d main(StringC1 args) throws IOException {
Chain chain = new Chain() ; i n t nwords = MAXGEN; chain. build(System.in) ; chain. generate(nwords) ;
I
SECTION 3.5
JAVA
73
When an instance of class Chain is created, it in turn creates a hash table and sets up the initial prefix of NPREF NONWORDs. The bui 1d function uses the library function StreamTokenizer to parse the input into words separated by white space characters. The three calls before the loop set the tokenizer into the proper state for our definition of "word."
// Chain b u i l d : b u i l d S t a t e t a b l e from i n p u t stream v o i d bui 1d(1nputSt ream i n ) throws IOExcepti on
t StreamTokenizer s t = new StreamTokenizer(in); st. resetsyntax0 ; st.wordChars(0, Character.MAX-VALUE); s t .whi tespaceChars(0, ' ') ; w h i l e (st.nextToken() != st.TT-EOF) add(st.sva1) ; add(NONWORD) ;
// remove d e f a u l t r u l e s // t u r n on a l l chars // except up t o blank
I The add function retrieves the vector of suffixes for the current prefix from the hash table; if there are none (the vector is null), add creates a new vector and a new prefix to store in the hash table. In either case, it adds the new word to the suffix vector and advances the prefix by dropping the first word and adding the new word at the end. // Chain add: add word t o s u f f i x l i s t , update p r e f i x v o i d add(Stri ng word)
f Vector s u f = (Vector) statetab.get(prefix) ; i f (suf == n u l l ) { s u f = new V e c t o r 0 ; s t a t e t a b . put(new P r e f i x ( p r e f i x ) , suf) ;
I
suf. addElement(word) ; p r e f i x . p r e f . removeEl ementAt (0) ; p r e f i x . p r e f .addElement(word);
I Notice that if s u f is null, add installs a new P r e f i x in the hash table, rather than p r e f i x itself. This is because the Hashtable class stores items by reference, and if we don't make a copy, we could overwrite data in the table. This is the same issue that we had to deal with in the C program. The generation function is similar to the C version, but slightly more compact because it can index a random vector element directly instead of looping through a list.
74
DESIGN AND IMPLEMENTATION
CHAPTER 3
// Chain generate: generate output words void generate(i n t nwords)
C p r e f i x = new Prefix(NPREF, NONWORD) ; f o r ( i n t i = 0; i < nwords; i++) Vector s = (Vector) statetab.get(prefix) ; i n t r = Math.abs(rand.nextInt()) % s.size(); S t r i n g s u f = (String) s .elementAt(r) ; i f (suf . equal s(NONWORD)) break; System.out. p r i n t l n ( s u f ) ; p r e f i x . p r e f . removeElementAt(0) ; p r e f i x -p r e f .addElement(suf) ;
1 1 The two constructors of P r e f i x create new instances from supplied data. The first copies an existing Prefix, and the second creates a prefix from n copies o f a string; we use i t to make NPREF copies of NONWORD when initializing:
// P r e f i x constructor: duplicate e x i s t i n g p r e f i x P r e f i x ( P r e f i x p)
C p r e f = (Vector)
p.pref.clone0;
1 // P r e f i x constructor: n copies o f s t r P r e f i x ( i n t n, S t r i n g s t r )
C p r e f = new Vector(); f o r ( i n t i = 0; i < n; i++) p r e f . addElement(str) ;
1 Prefix also has two methods, has hCode and equal s, that are called implicitly by the implementation of Hashtabl e to index and search the table. I t is the need to have an explicit class for these two methods for Hashtabl e that forced us to make P r e f i x a full-fledged class. rather than just a Vector like the suffix. The hashcode method builds a single hash value by combining the set of hashcodes for the elements of the vector: s t a t i c f i n a l i n t MULTIPLIER = 31;
// f o r hashcode0
// P r e f i x hashcode: generate hash from a l l p r e f i x words p u b l i c i n t hashcode() {
i n t h = 0; f o r ( i n t i = 0; i < pref.size(); i++) h = MULTIPLIER * h + p r e f .elementAt(i) .hashcode(); r e t u r n h;
1
SECTION 3.5
JAVA
75
and equal s does an elementwise comparison of the words in two prefixes:
// Prefix equals: compare two prefixes for equal words pub1 i c boolean equal s(0bject o) {
Prefix p = (Prefix) o; f o r ( i n t i = 0; i < pref.size(); i++) i f ( ! pref. el ementAt(i) .equal s(p. pref. el ementAt(i))) return f a l s e ; return t r u e ;
1 The Java program is significantly smaller than the C program and takes care of more details; Vectors and the Hashtabl e are the obvious examples. In general, storage management is easy since vectors grow as needed and garbage collection takes care of reclaiming memory that is no longer referenced. But to use the Hashtable class, we still need to write functions hashcode and equals, so Java isn't taking care of all the details. Comparing the way the C and Java programs represent and operate on the same basic data structure, we see that the Java version has better separation of functionality. For example, to switch from Vectors to arrays would be easy. In the C version. everything knows what everything else is doing: the hash table operates on arrays that are maintained in various places, 1ookup knows the layout of the State and Suffix structures, and everyone knows the size of the prefix array. java Markov <jr-chemistry. t x t I fmt Wash the blackboard. Watch i t dry. The water goes i n t o the a i r . When water goes i n t o the a i r i t evaporates. Tie a damp cloth t o one end of a solid or liquid. Look around. What are the solid things? Chemical changes take place when something burns. I f the burning materi a1 has 1i q u i ds, they are stab1e and the sponge r i s e . I t looked l i k e dough, but i t i s burning. Break up the lump of sugar i n t o small pieces and put them together again i n the bottom of a liquid.
%
Exercise 3-4. Revise the Java version of markov to use an array instead of a Vector for the prefix in the State class.
76
DESIGN AND IMPLEMENTATION
CHAPTER 3
Our third implementation is in C++. Since C++ is almost a superset of C, it can be used as if it were C with a few notational conveniences, and our original C version of markov is also a legal C++ program. A more appropriate use of C++, however, would be to define classes for the objects in the program, more or less as we did in Java; this would let us hide implementation details. We decided to go even further by using the Standard Template Library or STL, since the STL has built-in mechanisms that will do much of what we need. The IS0 standard for C++ includes the STL as part of the language definition. The STL provides containers such as vectors, lists, and sets, and a family of fundamental algorithms for searching, sorting, inserting, and deleting. Using the template features of C++, every STL algorithm works on a variety of containers, including both user-defined types and built-in types like integers. Containers are expressed as C++ templates that are instantiated for specific data types; for example, there is a vector container that can be used to make particular types like vector or v e c t o r < s t r i ng>. All vector operations, including standard algorithms for sorting, can be used on such data types. In addition to a vector container that is similar to Java's Vector, the STL provides a deque container. A deque (pronounced "deck") is a double-ended queue that matches what we do with prefixes: it holds NPREF elements, and lets us pop the first element and add a new one to the end, in 0 ( 1) time for both. The STL deque is more general than we need, since it permits push and pop at either end, but the performance guarantees make it an obvious choice. The STL also provides an explicit map container, based on balanced trees, that stores key-value pairs and provides O(1ogn) retrieval of the value associated with any key. Maps might not be as efficient as O(1) hash tables, but it's nice not to have to write any code whatsoever to use them. (Some non-standard C++ libraries include a hash or hash-map container whose performance may be better.) We also use the built-in comparison functions, which in this case will do string comparisons using the individual strings in the prefix. With these components in hand, the code goes together smoothly. Here are the declarations: typedef deque<stri ng> P r e f i x ; map > s t a t e t a b ; // p r e f i x -> s u f f i x e s
The STL provides a template for deques; the notation dequexstri ng> specializes it to a deque whose elements are strings. Since this type appears several times in the program, we used a typedef to give it the name P r e f i x . The map type that stores prefixes and suffixes occurs only once, however, so we did not give it a separate name; the map declaration declares a variable s t a t e t a b that is a map from prefixes to vectors of strings. This is more convenient than either C or Java, because we don't need to provide a hash function or equals method.
SECTION 3.6
C++
77
The main routine initializes the prefix, reads the input (from standard input, called c i n in the C++ iostream library), adds a tail, and generates the output, exactly as in
the earlier versions:
// markov main: markov-chain random t e x t generation in t mai n(voi d) i n t nwords = MAXGEN; Prefix prefix;
// current i n p u t p r e f i x
// s e t up i n i t i a l p r e f i x f o r ( i n t i = 0; i < NPREF; i++) add (p r e f ix , NONWORD) ; b u i l d ( p r e f i x , cin); add(pref ix , NONWORD) ; generate(nwords); r e t u r n 0;
1 The function b u i l d uses the iostream library to read the input one word at a time:
// b u i l d : read i n p u t words, b u i l d s t a t e t a b l e v o i d build(Prefix& p r e f i x , istream& i n ) { s t r i n g buf; while ( i n >> buf) add(prefi x, buf) ;
1 The string buf will grow as necessary to handle input words of arbitrary length. The add function shows more of the advantages of using the STL:
// add: add word t o s u f f i x l i s t , update p r e f i x v o i d add(Prefix& p r e f i x , const s t r i n g & s)
I i f ( p r e f i x . size() == NPREF) { statetabCprefix1. push-back(s) ; p r e f i x .pop-f r o n t () ;
1 prefix.push-back(s);
1 Quite a bit is going on under these apparently simple statements. The map container overloads subscripting (the [Ioperator) to behave as a lookup operation. The expression s t a t e t a b[ p r e f i XI does a lookup in statetab with p r e f i x as key and returns a reference to the desired entry; the vector is created if it does not exist already. The push-back member functions of vector and deque push a new string onto the back end of the vector or deque; pop-f r o n t pops the first element off the deque. Generation is similar to the previous versions:
CHAPTER 3
// generate: produce output, one word per l i n e void generate(i n t nwords) { Prefix prefix; int i ; f o r (i = 0 ; i < NPREF; i++) // r e s e t i n i t i a l p r e f i x add(prefix. NONWORD); f o r (i = 0 ; i < nwords; i++) { v e c t o r < s t r i ng>& suf = s t a t e t a b [ p r e f i x ] ; const string& w = suf [rand() % suf .size()] ; i f (W == NONWORD) break; cout [ $ r ] points to the r-th suffix. Both the Perl and Awk programs are short compared to the three earlier versions. but they are harder to adapt to handle prefixes that are not exactly two words. The core of the C++ STL implementation (the add and generate functions) is of comparable length and seems clearer. Nevertheless, scripting languages are often a good choice for experimental programming, for making prototypes, and even for production use if run-time is not a major issue. Exercise 3-7. Modify the Awk and Perl versions to handle prefixes of any length. Experiment to determine what effect this change has on performance.
3.8 Performance We have several implementations to compare. We timed the programs on the Book of Psalms from the King James Bible, which has 42,685 words (5,238 distinct words, 22,482 prefixes). This text has enough repeated phrases ("Blessed is the ...")
PERFORMANCE
SECTION 3.8
81
that one suffix list has more than 400 elements, and there are a few hundred chains with dozens of suffixes, so it is a good test data set. Blessed is the man of the net. Turn thee unto me, and raise me up, that I may tell all my fears. They looked unto him, he heard. My praise shall be blessed. Wealth and riches shall be saved. Thou hast dealt well with thy hid treasure: they are cast into a standing water, the flint into a standing water, and dry ground into watersprings. The times in the following table are the number of seconds for generating 10.000 words of output; one machine is a 250MHz MIPS RlOOOO running Irix 6.4 and the other is a 400MHz Pentium I1 with 128 megabytes of memory running Windows NT. Run-time is almost entirely determined by the input size; generation is very fast by comparison. The table also includes the approximate program size in lines of source code.
C Java C++/STL/deque C++/STL/list Awk Perl
250MHz RlOOOO
4OOMHz Pentium I1
Lines of source code
0.36 sec 4.9 2.6 1.7 2.2 1.8
0.30 sec 9.2 11.2 1.5 2.1 1.O
150 105 70 70 20 18
The C and C++ versions were compiled with optimizing compilers. while the Java runs had just-in-time compilers enabled. The Irix C and C++ times are the fastest obtained from three different compilers; similar results were observed on Sun SPARC and DEC Alpha machines. The C version of the program is fastest by a large factor; Perl comes second. The times in the table are a snapshot of our experience with a particular set of compilers and libraries, however, so you may see very different results in your environment. Something is clearly wrong with the STL deque version on Windows. Experiments showed that the deque that represents the prefix accounts for most of the runtime, although it never holds more than two elements; we would expect the central data structure, the map, to dominate. Switching from a deque to a list (which is a doubly-linked list in the STL) improves the time dramatically. On the other hand, switching from a map to a (non-standard) hash container made no difference on Irix; hashes were not available on our Windows machine. It is a testament to the fundamental soundness of the STL design that these changes required only substituting the word l i s t for the word deque or hash for map in two places and recompiling. We conclude that the STL, which is a new component of C++, still suffers from immature implementations. The performance is unpredictable between implementations of the STL and between individual data structures. The same is true of Java, where implementations are also changing rapidly.
82
DESIGN AND IMPLEMENTATION
CHAPTER 3
There are some interesting challenges in testing a program that is meant to produce voluminous random output. How do we know it works at all? How do we know it works all the time? Chapter 6, which discusses testing, contains some suggestions and describes how we tested the Markov programs.
3.9 Lessons The Markov program has a long history. The first version was written by Don P. Mitchell. adapted by Bruce Ellis. and applied to humorous deconstructionist activities throughout the 1980s. It lay dormant until we thought to use it in a university course as an illustration of program design. Rather than dusting off the original. we rewrote it from scratch in C to refresh our memories of the various issues that arise, and then wrote it again in several other languages, using each language's unique idioms to express the same basic idea. After the course, we reworked the programs many times to improve clarity and presentation. Over all that time, however, the basic design has remained the same. The earliest version used the same approach as the ones we have presented here, although it did employ a second hash table to represent individual words. If we were to rewrite it again. we would probably not change much. The design of a program is rooted in the layout of its data. The data structures don't define every detail, but they do shape the overall solution. Some data structure choices make little difference, such as lists versus growable arrays. Some implementations generalize better than others-the Per1 and Awk code could be readily modified to one- or three-word prefixes but parameterizing the choice would be awkward. As befits object-oriented languages, tiny changes to the C++ and Java implementations would make the data structures suitable for objects other than English text, for instance programs (where white space would be significant), or notes of music. or even mouse clicks and menu selections for generating test sequences. Of course, while the data structures are much the same, there is a wide variation in the general appearance of the programs, in the size of the source code, and in performance. Very roughly, higher-level languages give slower programs than lower level ones, although it's unwise to generalize other than qualitatively. Big building-blocks like the C++ STL or the associative arrays and string handling of scripting languages can lead to more compact code and shorter development time. These are not without price, although the performance penalty may not matter much for programs. like Markov, that run for only a few seconds. Less clear, however, is how to assess the loss of control and insight when the pile of system-supplied code gets so big that one no longer knows what's going on underneath. This is the case with the STL version; its performance is unpredictable and there is no easy way to address that. One immature implementation we used needed
SECTION 3.9
LESSONS
83
to be repaired before it would run our program. Few of us have the resources or the energy to track down such problems and fix them. This is a pervasive and growing concern in software: as libraries, interfaces, and tools become more complicated. they become less understood and less controllable. When everything works, rich programming environments can be very productive, but when they fail, there is little recourse. Indeed. we may not even realize that something is wrong if the problems involve performance or subtle logic errors. The design and implementation of this program illustrate a number of lessons for larger programs. First is the importance of choosing simple algorithms and data structures, the simplest that will d o the job in reasonable time for the expected problem size. If someone else has already written them and put them in a library for you, that's even better; our C++ implementation profited from that. Following Brooks's advice, we find it best to start detailed design with data structures, guided by knowledge of what algorithms might be used; with the data structures settled. the code goes together easily. It's hard to design a program completely and then build it; constructing real programs involves iteration and experimentation. The act of building forces one to clarify decisions that had previously been glossed over. That was certainly the case with our programs here, which have gone through many changes of detail. As much as possible, start with something simple and evolve it as experience dictates. If our goal had been just to write a personal version of the Markov chain algorithm for fun. we would almost surely have written it in Awk or Perl-though not with as much polishing as the ones we showed here-and let it go at that. Production code takes much more effort than prototypes do, however. If we think of the programs presented here as production code (since they have been polished and thoroughly tested), production quality requires one or two orders of magnitude more effort than a program intended for personal use.
Exercise 3-8. We have seen versions of the Markov program in a wide variety of languages, including Scheme. Tcl, Prolog, Python, Generic Java. ML, and Haskell; each presents its own challenges and advantages. Implement the program in your favorite language and compare its general flavor and performance.
Supplementary Reading The Standard Template Library is described in a variety of books, including Generic Prograrnming and the STL, by Matthew Austern (Addison-Wesley, 1998). The definitive reference on C++ itself is The C++ Prograrmning Language, by Bjarne Stroustrup (3rd edition, Addison-Wesley, 1997). For Java, we refer to The Java Prograntrrzing Language, 2nd Edition by Ken Arnold and James Gosling (AddisonWesley, 1998). The best description of Perl is Programnzi~gPerl, 2nd Edition, by Larry Wall, Tom Christiansen, and Randal Schwartz (O'Reilly, 1996).
84
DESIGN AND IMPLEMENTATION
CHAPTER 3
The idea behind design patterns is that there are only a few distinct design constructs in most programs in the same way that there are only a few basic data structures; very loosely, it is the design analog of the code idioms that we discussed in Chapter 1. The standard reference is Design Patterns: Elements of Reusable ObjectOriented Sofrware, by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley. 1995). The picaresque adventures of the markov program, originally called shaney, were described in the "Computing Recreations" column of the June. 1989 Scientific American. The article was republished in The Magic Machine, by A. K . Dewdney (W. H. Freeman, 1990).
Interfaces Before I built a wall I'd ask to know What I was walling in or walling out, And to whom I was like to give offence. Something there is that doesn't love a wall. That wants it down. Robert Frost, Mending Wall The essence of design is to balance competing goals and constraints. Although there may be many tradeoffs when one is writing a small self-contained system, the ramifications of particular choices remain within the system and affect only the individual programmer. But when code is to be used by others, decisions have wider repercussions. Among the issues to be worked out in a design are Interfaces: what services and access are provided? The interface is in effect a contract between supplier and customer. The desire is to provide services that are uniform and convenient, with enough functionality to be easy to use but not so much as to become unwieldy. Information hiding: what information is visible and what is private? An interface must provide straightforward access to the components while hiding details of the implementation so they can be changed without affecting users. Resource management: who is responsible for managing memory and other limited resources? Here, the main problems are allocating and freeing storage. and managing shared copies of information. Error handling: who detects errors. who reports them, and how? When an error is detected, what recovery is attempted? In Chapter 2 we looked at the individual pieces-the data structures-from which a system is built. In Chapter 3, we looked at how to combine those into a small program. The topic now turns to the interfaces between components that might come from different sources. In this chapter we illustrate interface design by building a
86
CHAPTER 4
INTERFACES
library of functions and data structures for a common task. Along the way, we will present some principles of design. Typically there are an enormous number of decisions to be made, but most are made almost unconsciously. Without these principles, the result is often the sort of haphazard interfaces that frustrate and impede programmers every day.
4.1 Comma-Separated Values Comma-separated values, or CSV, is the term for a natural and widely used representation for tabular data. Each row of a table is a line of text; the fields on each line are separated by commas. The table at the end of the previous chapter might begin this way in CSV format:
,"2SOMHz","400MHz","Lines o f " ,"RlOOOO","Pentium I I " , " s o u r c e code" C,0.36 s e c , 0 . 3 0 sec.150 lava,4.9.9.2,105 This format is read and written by programs such as spreadsheets; not coincidentally, it also appears on web pages for services such as stock price quotations. A popular web page for stock quotes presents a display like this:
Symbol LU T MSFT
Last Trade 86-114 60-1 1/16 106-9116
2: 19PM 2: 19PM 2:24PM
Change +4.94% - 1.92% + 1.31%
+4-1/16 - 1-3/16 + 1-318
Volume 5,804,800 2,468,000 1 1,474,900
Download Spreadsheet Format Retrieving numbers by interacting with a web browser is effective but timeconsuming. It's a nuisance to invoke a browser, wait, watch a barrage of advertisements, type a list of stocks, wait, wait, wait, then watch another barrage, all to get a few numbers. To process the numbers further requires even more interaction; selecting the "Download Spreadsheet Format" link retrieves a file that contains much the same information in lines of CSV data like these (edited to fit):
Conspicuous by its absence in this process is the principle of letting the machine do the work. Browsers let your computer access data on a remote server, but it would be more convenient to retrieve the data without forced interaction. Underneath all the
SECTION 4.2
A PROTOTYPE LIBRARY
87
button-pushing is a purely textual procedure-the browser reads some HTML, you type some text, the browser sends that to a server and reads some HTML back. With the right tools and language, it's easy to retrieve the information automatically. Here's a program in the language Tcl to access the stock quote web site and retrieve CSV data in the format above, preceded by a few header lines: g e t q u o t e s . t c l : s t o c k p r i c e s f o r Lucent, AT&T, M i c r o s o f t s e t SO [socket quote.yahoo.com 801 ;# connect t o s e r v e r s e t q "/d/quotes.csv?s=LU+T+MSFT&f=slldltlclohgv" p u t s $so "GET $q HTTP/l.O\r\n\r\n" ;# send r e q u e s t f l u s h $so ;# read & p r i n t rep1 y p u t s [read $so]
#
The cryptic sequence f = . . . that follows the ticker symbols is an undocumented control string, analogous to the first argument of p r i n t f , that determines what values to retrieve. By experiment, we determined that s identifies the stock symbol, 1 1 the last price, c l the change since yesterday, and so on. What's important isn't the details, which are subject to change anyway, but the possibility of automation: retrieving the desired information and converting it into the form we need without any human intervention. We can let the machine d o the work. It typically takes a fraction of a second to run g e t q u o t e s , far less than interacting with a browser. Once we have the data, we will want to process it further. Data formats like CSV work best if there are convenient libraries for converting to and from the format, perhaps allied with some auxiliary processing such as numerical conversions. But we d o not know of an existing public library to handle CSV, so we will write one ourselves. In the next few sections. we will build three versions of a library to read CSV data and convert it into an internal representation. Along the way, we'll talk about issues that arise when designing software that must work with other software. For example, there does not appear to be a standard definition of CSV. so the implementation cannot be based on a precise specification, a common situation in the design of interfaces.
4.2 A Prototype Library We are unlikely to get the design of a library or interface right on the first attempt. As Fred Brooks once wrote, "plan to throw one away; you will, anyhow." Brooks was writing about large systems but the idea is relevant for any substantial piece of software. It's not usually until you've built and used a version of the program that you understand the issues well enough to get the design right. In this spirit, we will approach the construction of a library for CSV by building one to throw away, a prototype. Our first version will ignore many of the difficulties of a thoroughly engineered library, but will be complete enough to be useful and to let us gain some familiarity with the problem.
88
INTERFACES
CHAPTER 4
Our starting point is a function csvgetl ine that reads one line of CSV data from a file into a buffer, splits it into fields in an array, removes quotes. and returns the number of fields. Over the years, we have written similar code in almost every language we know, so it's a familiar task. Here is a prototype version in C; we've marked it as questionable because it is just a prototype: char buf [ZOO] ; char afield[20] ;
/a /a
input l i n e buffer f i e 1 ds a/
a/
csvgetline: read and parse l i n e , r e t u r n f i e l d count sample i n p u t : "LU",86.25,"11/4/1998","2:19PM",+4.0625 in t csvgetl ine(F1LE * f i n )
/a /a
a/ a/
C i n t nfield; char *p, aq;
i f (fgets(buf. s i z e o f (buf) , f i n ) == NULL) r e t u r n -1; n f i e l d = 0; f o r (q = buf; (p=strtok(q, ",\n\rW)) != NULL; q = NULL) f i e l d [ n f i e l d++] = unquote(p) : return nfield;
1 The comment at the top of the function includes an example of the input format that the program accepts; such comments are helpful for programs that parse messy input. The CSV format is too complicated to be parsed easily by scanf so we use the C standard library function s t r t o k . Each call of s t r t o k ( p , s) returns a pointer to the first token within p consisting of characters not in s; s t r t o k terminates the token by overwriting the following character of the original string with a null byte. On the first call, strtok's first argument is the string to scan; subsequent calls use NULL to indicate that scanning should resume where it left off in the previous call. This is a poor interface. Because s t r t o k stores a variable in a secret place between calls, only one sequence of calls may be active at one time; unrelated interleaved calls will interfere with each other. Our function unquote removes the leading and trailing quotes that appear in the sample input above. It does not handle nested quotes. however, so although sufficient for a prototype, it's not general. unquote: remove leading and t r a i l i n g quote char aunquote(char ap)
/a
C
i f (pro] == '"') { i f (p[strlen(p)-l] p[strlen(p)-11 p++ ;
1 r e t u r n p;
1
== ' " ' ) = '\0';
a/
SECTION 4.2
A PROTOTYPE LIBRARY
89
A simple test program helps verify that c s v g e t line works: /a c s v t e s t main: t e s t c s v g e t l i n e f u n c t i o n in t mai n (voi d)
a/
{
i n t i,n f ; whi 1e ((nf = csvgetl i ne(stdi n)) ! = -1) f o r (i= 0; i < n f ; i++) p r i n t f ( " f i e l d [ % d ] = '%s'\nl', i, f i e l d [ i ] ) ; r e t u r n 0;
1 The p r i n t f encloses the fields in matching single quotes, which demarcate them and help to reveal bugs that handle white space incorrectly. We can now run this on the output produced by getquotes. t c l : % getquotes.tc1
...
field101 field[l] f i e l d [2] field[3] field[4] fieldC51 field[6] fieldC71 f i e l d[8l field[O] field[l]
= = = = = =
= = =
= =
I csvtest
'LU' '86.375' '11/5/1998' '1:OlPM' '-0.125' '86' '86.375' '85.0625' '2888600' 'T' '61.0625'
(We have edited out the HITP header lines.) Now we have a prototype that seems to work on data of the sort we showed above. But it might be prudent to try it on something else as well, especially if we plan to let others use it. We found another web site that downloads stock quotes and obtained a file of similar information but in a different form: camage returns (\r) rather than newlines to separate records, and no terminating camage return at the end of the file. We've edited and formatted it to fit on the page: " Ticker" , " Price" , "Change", "Open". "Prev Close", "Day High", "Day LowN,"52 Week HighW,"52 Week Low","Dividend", "Yi e l dm,"Vol ume" , "Average Vol ume" , "P/E" "LU",86.313,-0.188.86.000,86.500,86.438,85.063,108-50, 36.18,0.16,0.1.2946700,9675000,N/A "T",61.125,0.938,60.375,60.188,61.125,60.000,68.50,
46.50,1.32,2.1,3061000,4777000,17.0 "MSFT",107.000,1.500,105.313,105.500,107.188,105.250, 119.62,59.00,N/A,N/A,7977300,16965000,51.0
With this input, our prototype failed miserably.
90
INTERFACES
CHAPTER 4
We designed our prototype after examining one data source, and we tested it originally only on data from that same source. Thus we shouldn't be surprised when the first encounter with a different source reveals gross failings. Long input lines. many fields, and unexpected or missing separators all cause trouble. This fragile prototype might serve for personal use or to demonstrate the feasibility of an approach, but no more than that. It's time to rethink the design before we try another implementation. We made a large number of decisions, both implicit and explicit, in the prototype. Here are some of the choices that were made, not always in the best way for a general-purpose library. Each raises an issue that needs more careful attention. The prototype doesn't handle long input lines or lots of fields. It can give wrong answers or crash because it doesn't even check for overflows, let alone return sensible values in case of errors. The input is assumed to consist of lines terminated by newlines. Fields are separated by conlmas and surrounding quotes are removed. There is no provision for embedded quotes or commas. The input line is not preserved; it is overwritten by the process of creating fields. No data is saved from one input line to the next: if something is to be remembered, a copy must be made. Access to the fields is through a global variable, the f i e l d array, which is shared by csvgetl ine and functions that call it; there is no control over access to the field contents or the pointers. There is also no attempt to prevent access beyond the last field. The global variables make the design unsuitable for a multi-threaded environment or even for two sequences of interleaved calls. The caller must open and close files explicitly; csvgetl ine reads only from open files. Input and splitting are inextricably linked: each call reads a line and splits it into fields. regardless of whether the application needs that service. The return value is the number of fields on the line; each line must be split to compute this value. There is also no way to distinguish errors from end of file. There is no way to change any of these properties without changing the code. This long yet incomplete list illustrates some of the possible design tradeoffs. Each decision is woven through the code. That's fine for a quick job. like parsing one fixed format from a known source. But what if the format changes, or a comma appears within a quoted string, or the server produces a long line or a lot of fields? It may seem easy to cope, since the "library" is small and only a prototype anyway. Imagine, however, that after sitting on the shelf for a few months or years the code becomes part of a larger program whose specification changes over time. How will csvgetl i ne adapt? If that program is used by others, the quick choices made in the original design may spell trouble that surfaces years later. This scenario is representative of the history of many bad interfaces. It is a sad fact that a lot of quick and
SECTION 4.3
A LIBRARY FOR OTHERS
91
dirty code ends up in widely-used software, where it remains dirty and often not as quick as it should have been anyway.
4.3 A Library for Others Using what we learned from the prototype, we now want to build a library worthy of general use. The most obvious requirement is that we must make c s v g e t l i ne more robust so it will handle long lines or many fields; it must also be more careful in the parsing of fields. To create an interface that others can use, we must consider the issues listed at the beginning of this chapter: interfaces, information hiding, resource management, and error handling. The interplay among these strongly affects the design. Our separation of these issues is a bit arbitrary, since they are interrelated.
Interface. We decided on three basic operations: c h a r c-csvgetl ine(F1LE c - ) : read a new CSV line c h a r c - c s v f i e l d ( i n t n): return the n-th field of the current line i n t csvnf i e l d (voi d): return the number of fields on the current line What function value should c s v g e t l i ne return? It is desirable to return as much useful information as convenient, which suggests returning the number of fields, as in the prototype. But then the number of fields must be computed even if the fields aren't being used. Another possible value is the input line length, which is affected by whether the trailing newline is preserved. After several experiments, we decided that c s v g e t l i n e will return a pointer to the original line of input, or NULL if end of file has been reached.. We will remove the newline at the end of the line returned by c s v g e t l i ne, since it can easily be restored if necessary. The definition of a field is complicated; we have tried to match what we observe empirically in spreadsheets and other programs. A field is a sequence of zero or more characters. Fields are separated by commas. Leading and trailing blanks are preserved. A field may be enclosed in double-quote characters, in which case it may contain commas. A quoted field may contain double-quote characters, which are represented by a doubled double-quote; the CSV field " x " " y W defines the string x"y. Fields may be empty; a field specified as "" is empty, and identical to one specified by adjacent commas. Fields are numbered from zero. What if the user asks for a non-existent field by calling c s v f i e l d(-1) or c s v f i e l d (100000)? We could return " " (the empty string) because this can be printed and compared; programs that process variable numbers of fields would not have to take special precautions to deal with non-existent ones. But that choice provides no way to distinguish empty from non-existent. A second choice would be to print an error message or even abort; we will discuss shortly why this is
92
INTERFACES
CHAPTER 4
not desirable. We decided to return NULL, the conventional value for a non-existent string in C.
Information hiding. The library will impose no limits on input line length or number of fields. To achieve this, either the caller must provide the memory or the callee (the library) must allocate it. The caller of the library function f g e t s passes in an array and a maximum size. If the line is longer than the buffer, it is broken into pieces. This behavior is unsatisfactory for the CSV interface, so our library will allocate memory as it discovers that more is needed. Thus only c s v g e t l i ne knows about memory management; nothing about the way that it organizes memory is accessible from outside. The best way to provide that isolation is through a function interface: c s v g e t l i ne reads the next line, no matter how big, c s v f i e l d ( n ) returns a pointer to the bytes of the n-th field of the current line, and csvnf i e l d returns the number of fields on the current line. W e will have to grow memory as longer lines or more fields arrive. Details of how that is done are hidden in the c s v functions; no other part of the program knows how this works, for instance whether the library uses small arrays that grow, or very large arrays, or something completely different. Nor does the interface reveal when memory is freed. If the user calls only c s v g e t l i ne, there's no need to split into fields; lines can be split on demand. Whether field-splitting is eager (done right away when the line is read) or lazy (done only when a field or count is needed) or very lazy (only the requested field is split) is another implementation detail hidden from the user.
Resource management. We must decide who is responsible for shared information. Does c s v g e t l i ne return the original data or make a copy? W e decided that the return value of c s v g e t l i ne is a pointer to the original input, which will be overwritten when the next line is read. Fields will be built in a copy of the input line, and c s v f i e l d will return a pointer to the field within the copy. With this arrangement, the user must make another copy if a particular line or field is to be saved or changed, and it is the user's responsibility to release that storage when it is no longer needed. Who opens and closes the input file? Whoever opens an input file should d o the corresponding close: matching tasks should be done at the same level or place. We will assume that c s v g e t l i ne is called with a F I L E pointer to an already-open file that the caller will close when processing is complete. Managing the resources shared or passed across the boundary between a library and its callers is a difficult task, and there are often sound but conflicting reasons to prefer various design choices. Errors and misunderstandings about the shared responsibilities are a frequent source of bugs.
Error handling. Because c s v g e t l i ne returns NULL, there is no good way to distinguish end of file from an error like running out of memory; similarly, access to a non-existent field causes no error. By analogy with f e r r o r , we could add another function c s v g e t e r r o r to the interface to report the most recent error, but for simplicity we will leave it out of this version.
SECTION 4.3
A LIBRARY FOR OTHERS
93
As a principle, library routines should not just die when an error occurs; error status should be returned to the caller for appropriate action. Nor should they print messages or pop up dialog boxes, since they may be running in an environment where a message would interfere with something else. Error handling is a topic worth a separate discussion of its own, later in this chapter. Specification. The choices made above should be collected in one place as a specification of the services that csvgetl i ne provides and how it is to be used. In a large project, the specification precedes the implementation, because specifiers and implementers are usually different people and may be in different organizations. In practice, however. work often proceeds in parallel, with specification and code evolving together, although sometimes the "specification" is written only after the fact to describe approximately what the code does. The best approach is to write the specification early and revise it as we learn from the ongoing implementation. The more accurate and careful a specification is, the more likely that the resulting program will work well. Even for personal programs, it is valuable to prepare a reasonably thorough specification because it encourages consideration of alternatives and records the choices made. For our purposes, the specification would include function prototypes and a detailed prescription of behavior, responsibilities and assumptions: Fields are separated by commas. A field may be enclosed in double-quote characters "...". A quoted field may contain commas but not newlines. A quoted field may contain double-quote characters ",represented by "". Fields may be empty; "" and an empty string both represent an empty field. Leading and trailing white space is preserved. char acsvgetli ne(F1LE af) ; reads one line from open input file f ; assumes that input lines are terminated by \r, \n, \r\n, or EOF. returns pointer to line, with terminator removed, or NULL if EOF occurred. line may be of arbitrary length; returns NULL if memory limit exceeded. line must be treated as read-only storage; caller must make a copy to preserve or change contents. char acsvf i el d ( i n t n) ; fields are numbered from 0. returns n-th field from last line read by csvgetl i ne; returns NULL if n < 0 or beyond last field. fields are separated by commas. fields may be surrounded by "..."; such quotes are removed; within "... "," " is replaced by " and comma is not a separator. in unquoted fields, quotes are regular characters. there can be an arbitrary number of fields of any length; returns NULL if memory limit exceeded. field must be treated as read-only storage; caller must make a copy to preserve or change contents. behavior undefined if called before csvgetl i ne is called.
94
INTERFACES
CHAPTER 4
in t c s v n f i e l d(void) ;
returns number of fields on last line read by csvgetline. behavior undefined if called before csvgetline is called. This specification still leaves open questions. For example, what values should be returned by csvfie l d and csvnfie l d if they are called after csvgetline has encountered EOF? How should ill-formed fields be handled? Nailing down all such puzzles is difficult even for a tiny system, and very challenging for a large one, though it is important to try. One often doesn't discover oversights and omissions until implementation is underway. The rest of this section contains a new implementation of csvgetline that matches the specification. The library is broken into two files, a header csv. h that contains the function declarations that represent the public part of the interface, and an implementation file csv . c that contains the code. Users include csv. h in their source code and link their compiled code with the compiled version of csv. c; the source need never be visible. Here is the header file:
/* csv.h: i n t e r f a c e f o r csv l i b r a r y extern char acsvgetline(F1LE n f ) ; extern char acsvfi e l d ( i n t n) ; extern i n t csvnfield(void) ;
/n /a /a
a/
read next i n p u t l i n e n/ r e t u r n f i e l d n a/ r e t u r n number o f f i e l d s
a/
The internal variables that store text and the internal functions like s p l i t are declared s t a t i c so they are visible only within the file that contains them. This is the simplest way to hide information in a C program. enum
C
static static static static static static
NOMEM = -2 char char int char int int
3;
*line asline maxline *afield maxfield nfield
/n
out o f memory signal
= NULL; = NULL;
/n
0; NULL; = 0; = 0;
/* /a
i n p u t chars n/ l i n e copy used by s p l i t a/ s i z e o f l i n e [ ] and s l i n e [ ] f i e l d pointers */ size o f f i e l d [ ] a/ number o f f i e l d s i n f i e l d [ ]
/a
f i e l d separator chars
= =
s t a t i c char fieldsep[] = " ,";
/a
/a /a
a/
a/
a/
*/
The variables are initialized statically as well. These initial values are used to test whether to create or grow arrays. These declarations describe a simple data structure. The 1ine array holds the input line; the s line array is created by copying characters from 1ine and terminating each field. The f i e l d array points to entries in s line. This diagram shows the state of these three arrays after the input line ab , "cd" , "en "f", , "g, h" has been processed. Shaded elements in s line are not part of any field.
A LIBRARY FOR OTHERS
SECTION 4.3
line
a b , " c d " , " e " " f " , , "
sl i ne
a b \Om c
field
0
t
95
d \ O \ O m e " f \ O " \ 0 \ 0 " g , h \O
t
1
t t
f2
3
4
Here is the function csvgetline itself: csvgetline: g e t one l i n e , grow as needed a/ sample i n p u t : "LU".86.25,"11/4/1998","2:19PM",+4.0625 char acsvgetl ine(F1LE a f i n)
/a
/a
i
*/
i n t i,c; char anew1, anews; i f ( l i n e == NULL) { /a a l l o c a t e on f i r s t c a l l a/ maxline = maxfield = 1; l i n e = (char a) malloc(max1ine); s l i n e = (char a) malloc(max1ine) ; f i e l d = (char a*) ma1loc(maxfieldnsizeof(field[0])); i f ( l i n e == NULL I I s l i n e == NULL I I f i e l d == NULL) { reset 0 ; /a out o f memory */ r e t u r n NULL;
I
1
f o r (i=O; (c=getc (f in)) ! =EOF && ! endofl in e ( f i n , c) ; i++) { i f (i>= maxline-1) { /n grow l i n e */ /a double current s i z e */ maxline a= 2; newl = (char a) real l o c ( l i n e , maxli ne) ; news = (char a) realloc(s1ine, maxl ine) ; i f (newl == NULL ( I news == NULL) { reset() ; r e t u r n NULL; /a out o f memory a/
3
1ine = newl ; s l i n e = news;
3 l i n e [ i ] = c;
1
l i n e [ i ] = '\O'; i f ( s p l i t ( ) == NOMEM) { reset (1 ; r e t u r n NULL;
/a
out o f memory
*/
3 r e t u r n (c == EOF && i == 0) ? NULL : l i n e ;
1 An incoming line is accumulated in 1ine, which is grown as necessary by a call to r e a l loc; the size is doubled on each growth, as in Section 2.6. The s line array is
96
INTERFACES
CHAPTER 4
kept the same size as 1ine; c s v g e t line calls s p l i t to create the field pointers in a separate array fie l d, which is also grown as needed. As is our custom, we start the arrays very small and grow them on demand, to guarantee that the array-growing code is exercised. If allocation fails, we call r e s e t to restore the globals to their starting state, so a subsequent call to c s v g e t line has a chance of succeeding: /a r e s e t : s e t v a r i a b l e s back t o s t a r t i n g values s t a t i c v o i d reset(void)
C
a/
free(1ine) ; /a free(NULL1 p e r m i t t e d by ANSI C free(s1ine) ; f r e e (fie l d) ; l i n e = NULL; s l i n e = NULL; f i e l d = NULL; maxline = m a x f i e l d = n f i e l d = 0;
a/
I The endof 1ine function handles the problem that an input line may be terminated by a carriage return, a newline, both, or even EOF: /a e n d o f l i n e : check f o r and consume \r, \n, s t a t i c i n t endofline(F1LE a f i n , i n t c)
\r\n,
o r EOF
a/
C in t eol ; eol = (c=='\rl I I c=='\nl); i f (c == ' \ r g ) { c = getc(fin) ; i f (c != '\n' && c != EOF) ungetc(c, f i n ) ; /a read t o o f a r ; p u t c back
a/
1 return eol;
I A separate function is necessary. since the standard input functions do not handle the rich variety of perverse formats encountered in real inputs. Our prototype used s t r t o k to find the next token by searching for a separator character, normally a comma, but this made it impossible to handle quoted commas. A major change in the implementation of s p l i t is necessary, though its interface need not change. Consider these input lines:
Each line has three empty fields. Making sure that s p l i t parses them and other odd inputs correctly complicates it significantly, an example of how special cases and boundary conditions can come to dominate a program.
SECTION 4.3
97
A LIBRARY FOR OTHERS
/* s p l i t : s p l i t l i n e i n t o f i e l d s s t a t i c in t spl i t ( v o i d )
a/
C char ap, tanewf; char asepp; /a p o i n t e r t o temporary separator character i n t sepc; /a temporary separator character */
a/
n f i e l d = 0; i f (line[Ol == '\O') r e t u r n 0; strcpy(sline, l i n e ) ; p = sline; do
C .if ( n f i e l d >= maxfield) { maxfi e l d a= 2 ; /a double current size newf = (char a*) r e a l l o c ( f i e l d , maxfield a sizeof(field[O])); i f (newf == NULL) r e t u r n NOMEM; f i e l d = newf:
*/
1
if (ap == '"') sepp = advquoted(++p) ; /a s k i p i n i t i a l quote else sepp = p + strcspn(p, fieldsep); sepc = sepp[O] ; seppCO] = '\0' ; /a terminate f i e l d a/ f i e l d [ n f i e l d++] = p; p = sepp + 1; ) while (sepc == ' , ' ) ;
a/
return nfield;
I The loop grows the array of field pointers if necessary, then calls one of two other functions to locate and process the next field. If the field begins with a quote, advquoted finds the field and returns a pointer to the separator that ends the field. Otherwise, to find the next comma we use the library function strcspn(p, s), which searches a string p for the next occurrence of any character in string s; it returns the number of characters skipped over. Quotes within a field are represented by two adjacent quotes, so advquoted squeezes those into a single one; it also removes the quotes that surround the field. Some complexity is added by an attempt to cope with plausible inputs that don't match the specification. such as "abcWdef. In such cases, we append whatever follows the second quote until the next separator as part of this field. Microsoft Excel appears to use a similar algorithm.
98
INTERFACES
CHAPTER 4
/n advquoted: quoted f i e l d ; r e t u r n p o i n t e r t o next separator s t a t i c char nadvquoted (char np)
*/
C i n t i, j; f o r (i= j = 0; p [ j ] != ' \ O 1 ; i++, j++){ i f ( p [ j ] == ' " ' && p[++j] != ' " ' ) { /a copy up t o next separator o r \O i n t k = strcspn(p+j, fieldsep); memmove (p+i , p+ j , k) ; i += k; j += k; break;
a/
1
~ C i =l PUI;
p[i] = '\09; r e t u r n p + j;
1 Since the input line is already split, csvfie l d and csvnfie l d are trivial: /n c s v f i e l d : r e t u r n p o i n t e r t o n- th f i e l d char * c s v f i e l d ( i n t n)
C
*/
i f (n < 0 I I n >= n f i e l d ) r e t u r n NULL; return field[n] ;
1 /a csvnfield: r e t u r n number o f f i e l d s in t csvnfie l d(voi d)
*/
C return n f i e l d ;
1 Finally, we can modify the test driver to exercise this version of the library; since it keeps a copy of the input line, which the prototype does not. it can print the original line before printing the fields: csvtest main: t e s t CSV l i b r a r y n/ in t mai n(voi d)
/a
C i n t i; char * l i n e ; while ( ( l i n e = csvgetline(stdin)) != NULL) { p r i n t f ( " l i n e = '%sl\n", l i n e ) ; f o r (i= 0; i c c s v n f i e l d o ; i++) p r i n t f ( " f i e l d [ % d ] = '%s'\nW, i, c s v f i e l d ( i ) ) ;
1 r e t u r n 0;
1
SECTION 4.4
A C++ IMPLEMENTATION
99
This completes our C version. It handles arbitrarily large inputs and does something sensible even with perverse data. The price is that it is more than four times as long as the first prototype and some of the code is intricate. Such expansion of size and complexity is a typical result of moving from prototype to production.
Exercise 4-1. There are several degrees of laziness for field-splitting; among the possibilities are to split all at once but only when some field is requested, to split only the field requested, or to split up to the field requested. Enumerate possibilities, assess their potential difficulty and benefits, then write them and measure their speeds. Exercise 4-2. Add a facility so separators can be changed (a) to an arbitrary class of characters; (b) to different separators for different fields; (c) to a regular expression (see Chapter 9). What should the interface look like? Exercise 4-3. We chose to use the static initialization provided by C as the basis of a one-time switch: if a pointer is NULL on entry. initialization is performed. Another possibility is to require the user to call an explicit initialization function, which could include suggested initial sizes for arrays. Implement a version that combines the best of both. What is the role of r e s e t in your implementation? Exercise 4-4. Design and implement a library for creating CSV-formatted data. The simplest version might take an array of strings and print them with quotes and commas. A more sophisticated version might use a format string analogous to p r i n t f . Look at Chapter 9 for some suggestions on notation.
4.4 A C++ Implementation In this section we will write a C++ version of the CSV library to address some of the remaining limitations of the C version. This will entail some changes to the specification, of which the most important is that the functions will handle C++ strings instead of C character arrays. The use of C++ strings will automatically resolve some of the storage management issues, since the library functions will manage the memory for us. In particular. the field routines will return strings that can be modified by the caller, a more flexible design than the previous version. A class Csv defines the public face, while neatly hiding the variables and functions of the implementation. Since a class object contains all the state for an instance, we can instantiate multiple Csv variables; each is independent of the others so multiple CSV input streams can operate at the same time.
100
INTERFACES
CHAPTER 4
class Csv { // read and parse comma-separated values // sample i n p u t : "LU",86.25,"11/4/1998","2:19PM",+4.0625 public: Csv(istream& f i n = c i n , s t r i n g sep = ",") : f i n ( f i n) , f i e l dsep(sep) 1) i n t getline(string&) ; s t r i n g g e t f i e l d ( i n t n); i n t g e t n f i e l d o const { r e t u r n n f i e l d ; } private: istream& f i n ; string line; v e c t o r < s t r i ng> f i e l d ; i n t nfield; s t r i n g fieldsep; int in t int int
// // // // //
input f i l e pointer input l i n e f i e l d strings number o f f i e l d s separator characters
split(); endof 1ine (char) ; advplain(const s t r i n g & l i n e , s t r i n g & f l d , i n t ) ; advquoted(const s t r i n g & l i n e , s t r i n g & f l d , i n t ) ;
I; Default parameters for the constructor are defined so a default Csv object will read from the standard input stream and use the normal field separator; either can be replaced with explicit values. To manage strings, the class uses the standard C++ s t r i n g and vector classes rather than C-style strings. There is no non-existent state for a s t r i n g : "empty" means only that the length is zero, and there is no equivalent of NULL, so we can't use that as an end of file signal. Thus Csv: :get1 i n e returns the input line through an argument by reference, reserving the function value itself for end of file and error reports.
// g e t l i n e : get one l i n e , grow as needed i n t Csv: :getline(string& s t r )
1 char c; f o r ( l i n e = ""; fin.get(c) l i n e += c; split(); s t r = line; r e t u r n ! f i n . eof () ;
&& !endofline(c);
)
I The += operator is overloaded to append a character to a string. Minor changes are needed in endofline. Again, we have to read the input a character at a time, since none of the standard input routines can handle the variety of inputs.
SECTION 4.4
A C++ IMPLEMENTATION
// e n d o f l i n e : check f o r and consume \r, \n, in t Csv: : endofli neCchar c)
\r\n,
101
o r EOF
C in t eol ;
eel = (c=='\rl I I c=='\n'); i f (c == ' \ r ' ) fin.get(c) ; if ( ! f i n . e o f ( ) && c != '\nl) fin.putback(c1; // read t o o f a r return eol;
1 Here is the new version of s p l i t :
// s p l i t : s p l i t l i n e i n t o f i e l d s i n t Csv: : s p l i t ( )
C string fld; i n t i,j; n f i e l d = 0; i f (line.length() r e t u r n 0; i = 0; do
== 0)
C
i f (i< l i n e . l e n g t h ( ) && l i n e [ i ] == ' " ' ) j = advquoted(1ine. f l d . ++i); // s k i p quote else j = a d v p l a i n ( l i n e , f l d , i); i f ( n f i e l d >= f i e l d . s i z e ( ) ) field.push-back(f1d) ; e l se fieldCnfield] = f l d ; n fie l d++; i = j + l ; ) while (j < line.length()); return nfield;
Since s t r c s p n doesn't work on C++ strings, we must change both s p l i t and advquoted. The new version of advquoted uses the C++ standard function f i n d - f ir s t - o f to locate the next occurrence of a separator character. The call s . find-f ir s t - o f (fie l dsep, j) searches the string s for the first instance of any character in fie l dsep that occurs at or after position j. If it fails to find an instance, it returns an index beyond the end of the string, so we must bring it back within range. The inner f o r loop that follows appends characters up to the separator to the field being accumulated in f 1d.
102
INTERFACES
CHAPTER 4
// advquoted: quoted f i e l d ; r e t u r n index o f n e x t separator i n t Csv: :advquoted(const s t r i n g & s, s t r i n g & f l d , i n t i) i n t j; f l d = "". f o r ( j = i;j < s.length(); j++){ i f ( s [ j ] == ' " ' && s[++j] ! = ' " ' ) { i n t k = s.find-first-of(fieldsep, j); i f (k > s.length()) // no separator found k = s.length(); f o r (k -= j; k- - > 0; ) f l d += s[j++]; break :
1
fld
+=
s[j];
1
r e t u r n j;
1 The function f i n d - f i r s t - o f is also used i n a new function a d v p l a i n, which advances over a plain unquoted field. Again, this change is required because C string functions like s t r c s p n cannot be applied to C++ strings, which are an entirely different data type.
// advplain: unquoted f i e l d ; r e t u r n index o f next separator i n t Csv::advplain(const s t r i n g & s, s t r i n g & f l d , i n t i)
I i n t j; j = s . f i n d - f i r s t - o f( f i e l d s e p . i f ( j > s.length()) j = s.length(); f l d = s t r i n g ( s , i,j-i); return j ;
i); // l o o k f o r separator // none found
1 As before, Csv : : g e t fie l d is trivial, while Csv: : g e t n f i e l d is so short that it is implemented i n the class definition.
// g e t f i e l d : r e t u r n n - t h f i e l d s t r i n g Csv: : g e t f i e 1 d ( i n t n)
C
i f (n < 0 I I n >= n f i e l d ) r e t u r n ""; else return field[n] ;
1 Our test program is a simple variant o f the earlier one:
SECTION 4.5
// Csvtest main: t e s t Csv c l a s s i n t main(void) {
string line; Csv csv; while ( c s v . g e t l i n e ( l i n e ) != 0) { cout next) f r e e (PI;
Once memory has been freed, it must not be used since its contents may have changed and there is no guarantee that p->next still points to the right place. In some implementations of ma1 1oc and free. freeing an item twice corrupts the internal data structures hut doesn't cause trouble until much later, when a subsequent call slips on the mess made earlier. Some allocators come with debugging options that can be set to check the consistency of the arena at each call; turn them on if you have a non-deterministic bug. Failing that, you can write your own allocator that does some of its own consistency checking or logs all calls for separate analysis. An allocator that doesn't have to run fast is easy to write, so this strategy is feasible when the situation is dire. There are also excellent commercial products that check memory management and catch errors and leaks: writing your own ma1 1oc and f r e e can give you some of their benefits if you don't have access to them. When a program works for one person but fails for another, something must depend on the external environment of the program. This might include files read by the program, file permissions, environment variables, search path for commands, defaults, or startup files. It's hard to be a consultant for these situations, since you have to become the other person to duplicate the environment of the broken program.
Exercise 5-1. Write a version of ma1 loc and f r e e that can be used for debugging storage-management problems. One approach is to check the entire workspace on each call of ma11 oc and free; another is to write logging information that can be processed by another program. Either way, add markers to the beginning and end of each allocated block to detect overruns at either end.
5.6 Debugging Tools Debuggers aren't the only tools that help tind bugs. A variety of programs can help us wade through voluminous output to select important bits. find anomalies, or
132
CHAPTER 5
DEBUGGING
rearrange data to make it easier to see what's going on. Many of these programs are part of the standard toolkit; some are written to help find a particular bug or to analyze a specific program. In this section we will describe a simple program called s t r i ngs that is especially useful for looking at files that are mostly non-printing characters, such as executables or the mysterious binary formats favored by some word processors. There is often valuable information hidden within, like the text of a document, or error messages and undocumented options, or the names of files and directories, or the names of functions a program might call. We also find s t r i ngs helpful for locating text in other binary files. Image files often contain ASCII strings that identify the program that created them, and compressed files and archives (such as zip files) may contain file names; s t r i n g s will find these too. Unix systems provide an implementation of s t r i n g s already. although it's a little different from this one. It recognizes when its input is a program and examines only the text and data segments, ignoring the symbol table. Its -a option forces it to read the whole file. In effect, s t r i n g s extracts the ASCII text from a binary file so the text can be read or processed by other programs. If an error message carries no identification, it may not be evident what program produced it, let alone why. In that case, searching through likely directories with a command like % s t r i n g s a.exe * . d l 1
I
grep 'mystery message'
might locate the producer. The s t r i n g s function reads a file and prints all runs of at least MINLEN = 6 printable characters. /a s t r i n g s : e x t r a c t p r i n t a b l e s t r i n g s from stream */ v o i d strings(char *name, FILE * f i n )
C i n t c , i; char buf [BUFSIZ] ; do {
/* once f o r each s t r i n g a/ f o r (i = 0 ; (C = g e t c ( f i n ) ) != EOF; ) { i f (!isprint(c)) break; buf[i++] = c; i f (i >= BUFSIZ) break;
3
i f (i >= MINLEN)
3
1
/a p r i n t i f long enough printf("%s:%.*s\n", name, i , buf); w h i l e (c != EOF);
a/
SECTION 5.6
DEBUGGING TOOLS
133
The p r i n t f format string %.as takes the string length from the next argument (i), since the string (buf) is not null-terminated. The do-while loop finds and then prints each string, terminating at EOF. Checking for end of file at the bottom allows the getc and string loops to share a termination condition and lets a single p r i n t f handle end of string, end of file. and string too long. A standard-issue outer loop with a test at the top, or a single getc loop with a more complex body, would require duplicating the p r i n t f . This function started life that way, but it had a bug in the p r i n t f statement. We fixed that in one place but forgot to fix two others. ("Did I make the same mistake somewhere else?") At that point, it became clear that the program needed to be rewritten so there was less duplicated code; that led to the do-while. The main routine of s t r i n g s calls the s t r i n g s function for each of its argument files: / a s t r i n g s main: f i n d p r i n t a b l e s t r i n g s i n f i l e s i n t main(int argc, char aargv[])
I
a/
int i ; FILE a f i n ; setprogname("stri ngs") ; i f (argc == 1) e p r i n t f ("usage: s t r i n g s filenames") ; else { f o r (i = 1; i < argc; i++) { i f ( ( f i n = fopen(argv[i], "rb")) == NULL) w e p r i n t f ( " c a n ' t open %s:", a r g v [ i ] ) ; else { strings(argv[i] , fin); f c l o s e ( f i n) ;
1
1
1
return 0;
1 You might be surprised that s t r i n g s doesn't read its standard input if no files are named. Originally it did. To explain why it doesn't now, we need to tell a debugging story. The obvious test case for s t r i n g s is to run the program on itself. This worked fine on Unix. but under Windows 95 the command C:\>
s t r i n g s <strings.exe
produced exactly five lines of output:
134
DEBUGGING
CHAPTER 5
! T h i s program cannot be run i n DOS mode '.rdata @.data . id a t a . reloc
The first line looks like an error message and we wasted some time before realizing it's actually a string in the program, and the output is correct. at least as far as it goes. It's not unknown to have a debugging session derailed by misunderstanding the source of a message. But there should be more output. Where is it? Late one night, the light finally dawned. ("I've seen that before!") This is a portability problem that is described in more detail in Chapter 8. We had originally written the program to read only from its standard input using getchar. On Windows. however, getchar returns EOF when it encounters a particular byte ( O x l A or control-Z) in text mode input and this was causing the early termination. This is absolutely legal behavior, but not what we were expecting given our Unix background. The solution is to open the file in binary mode using the mode " r b " . But s t d i n is already open and there is no standard way to change its mode. (Functions like fdopen or setmode could be used but they are not part of the C standard.) Ultimately we face a set of unpalatable alternatives: force the user to provide a file name so it works properly on Windows but is unconventional on Unix; silently produce wrong answers if a Windows user attempts to read from standard input; or use conditional compilation to make the behavior adapt to different systems, at the price of reduced portability. We chose the first option so the same program works the same way everywhere.
Exercise 5-2. The s t r i n g s program prints strings with MINLEN or more characters, which sometimes produces more output than is useful. Provide s t r i n g s with an optional argument to define the minimum string length. Exercise 5-3. Write v i s , which copies input to output. except that it displays nonprintable bytes like backspaces, control characters. and non-ASCII characters as \Xhh where hh is the hexadecimal representation of the non-printable byte. By contrast with s t r i n g s , v i s is most useful for examining inputs that contain only a few nonprinting characters. Exercise 5-4. What does v i s produce if the itput is \XOA? How could you make the output of v i s unambiguous? Exercise 5-5. Extend v i s to process a sequence of files, fold long lines at any desired column, and remove non-printable characters entirely. What other features might be consistent with the role of the program?
SECTION 5.7
OTHER PEOPLE'S BUGS
135
5.7 Other People's Bugs Realistically, most programmers do not have the fun of developing a brand new system from the ground up. Instead, they spend much of their time using, maintaining. modifying and thus, inevitably, debugging code written by other people. When debugging others' code, everything that we have said about how to debug your own code applies. Before starting, though, you must first acquire some understanding of how the program is organized and how the original programmers thought and wrote. The term used in one very large software project is "discovery," which is not a bad metaphor. The task is discovering what on earth is going on in something that you didn't write. This is a place where tools can help significantly. Text-search programs like grep can find all the occurrences of names. Cross-referencers give some idea of the program's structure. A display of the graph of function calls is valuable if it isn't too big. Stepping through a program a function call at a time with a debugger can reveal the sequence of events. A revision history of the program may give some clues by showing what has been done to the program over time. Frequent changes are often a sign of code that is poorly understood or subject to changing requirements. and thus potentially buggy. Sometimes you need to track down errors in software you are not responsible for and d o not have the source code for. In that case, the task is to identify and characterize the bug sufficiently well that you can report it accurately. and at the same time perhaps find a "work-around" that avoids the problem. If you think that you have found a bug in someone else's program, the first step is to make absolutely sure it is a genuine bug, so you don't waste the author's time and lose your own credibility. When you find a compiler bug, make sure that the error is really in the compiler and not in your own code. For example, whether a right shift operation fills with zero bits (logical shift) or propagates the sign bit (arithmetic shift) is unspecified in C and C++, s o novices sometimes think it's an error if a construct like ? ?
i = -1; p r i n t f ("%d\nW, i >> 1) ;
yields an unexpected answer. But this is a portability issue, because this statement can legitimately behave differently on different systems. Try your test on multiple systems and be sure you understand what happens; check the language definition to be sure. Make sure the bug is new. Do you have the latest version of the program? IS there a list of bug fixes? Most software goes through n~ultiplereleases; if you find a bug in version 4.0b1, it might well be fixed or replaced by a new one in version 4.04b2. In any case, few programmers have much enthusiasm for fixing bugs in anything but the current version of a program.
136
DEBUGGING
CHAPTER 5
Finally, put yourself in the shoes of the person who receives your report. You want to provide the owner with as good a test case as you can manage. It's not very helpful if the bug can be demonstrated only with large inputs, or an elaborate environment, or multiple supporting files. Strip the test down to a minimal and selfcontained case. Include other information that could possibly be relevant, like the version of the program itself. and of the compiler. operating system. and hardware. For the buggy version of i s p r i n t mentioned in Section 5.4. we could provide this as a test program:
/* t e s t program f o r i s p r i n t bug i n t mai n (voi d)
a/
C i n t c; w h i l e ( i s p r i n t ( c = getchar()) p r i n t f ("%cW, c) ; return 0;
I I c != EOF)
3 Any line of printable text will serve as a test case, since the output will contain only half the input: % echo 1234567890 24680 %
1 isprint- test
The best bug reports are the ones that need only a line or two of input on a plain vanilla system to demonstrate the fault, and that include a fix. Send the kind of bug report you'd like to receive yourself.
5.8 Summary With the right attitude debugging can be fun, like solving a puzzle, but whether we enjoy it or not, debugging is an art that we will practice regularly. Still, it would be nice if bugs didn't happen, so we try to avoid them by writing code well in the first place. Well-written code has fewer bugs to begin with and those that remain are easier to find. Once a bug has been seen, the first thing to do is to think hard about the clues it presents. How could it have come about? Is it something familiar? Was something just changed in the program? Is there something special about the input data that provoked it? A few well-chosen test cases and a few print statements in the code may be enough. If there aren't good clues, hard thinking is still the best first step, to be followed by systematic attempts to narrow down the location of the problem. One step is cutting down the input data to make a small input that fails; another is cutting out code to eliminate regions that can't be related. It's possible to insert checking code that gets
SECTION 5.8
SUMMARY
137
turned on only after the program has executed some number of steps, again to try to localize the problem. A11 of these are instances of a general strategy, divide and conquer, which is as effective in debugging as it is in politics and war. Use other aids as well. Explaining your code to someone else (even a teddy bear) is wonderfully effective. Use a debugger to get a stack trace. Use some of the commercial tools that check for memory leaks, array bounds violations, suspect code, and the like. Step through your program when it has become clear that you have the wrong mental picture of how the code works. Know yourself, and the kinds of errors you make. Once you have found and fixed a bug, make sure that you eliminate other bugs that might be similar. Think about what happened so you can avoid making that kind of mistake again.
Supplementary Reading Steve Maguire's Writing Solid Code (Microsoft Press, 1993) and Steve McConnell's Code Complete (Microsoft Press, 1993) both have much good advice on debugging.
Testing In ordintiq cornputtitionti1 prtictice by hand or by desk mtichines, it is the custom to check every step of rhe comp~4rtiticmcind, when [in error is found, to localize it by ti h a c h a r d process stcirting from the.first poinr where the error is noted. Norbert Wiener, Cybernetics Testing and debugging are often spoken as a single phrase but they are not the same thing. To over-simplify, debugging is what you do when you know that a program is broken. Testing is a determined. systematic attempt to break a program that you think is working. Edsger Dijkstra made the famous observation that testing can demonstrate the presence of bugs, but not their absence. His hope is that programs can be made correct by construction, so that there are no errors and thus no need for testing. Though this is a fine goal, it is not yet realistic for substantial programs. So in this chapter we'll focus on how to test to find errors rapidly, efficiently, and effectively. Thinking about potential problems as you code is a good start. Systematic testing, from easy tests to elaborate ones, helps ensure that programs begin life working correctly and remain correct as they grow. Automation helps to eliminate manual processes and encourages extensive testing. And there are plenty of tricks of the trade that programmers have learned from experience. One way to write bug-free code is to generate it by a program. If some programming task is understood so well that writing the code seems mechanical. then it should be mechanized. A common case occurs when a program can be generated from a specification in some specialized language. For example, we compile high-level languages into assembly code; we use regular expressions to specify patterns of text; we use notations like SUM(A1:ASO) to represent operations over a range of cells in a spreadsheet. In such cases, if the generator or translator is correct and if the specification is correct, the resulting program will be correct too. We will cover this rich topic
140
TESTING
CHAPTER 6
in more detail in Chapter 9; in this chapter we will talk briefly about ways to create tests from compact specifications.
6.1 Test as You Write the Code The earlier a problem is found, the better. If you think systematically about what you are writing as you write it, you can verify simple properties of the program as it is being constructed, with the result that your code will have gone through one round of testing before it is even compiled. Certain kinds of bugs never come to life.
Test code at its boundaries. One technique is boundmy condirior7 testing: as each small piece of code is written-a loop or a conditional statement, for example+heck right then that the condition branches the right way or that the loop goes through the proper number of times. This process is called boundary condition testing because you are probing at the natural boundaries within the program and data, such as nonexistent or empty input. a single input item, an exactly full array, and s o on. The idea is that most bugs occur at boundaries. If a piece of code is going to fail, it will likely fail at a boundary. Conversely, if it works at its boundaries, it's likely to work elsewhere too. This fragment. modeled on fgets. reads characters until it finds a newline or fills a buffer: ?
?
i n t i; chars[MAX];
I
?
f o r (i= 0; ( s [ i ] = getchar())
!= '\n'
&& i < MAX-1; ++i)
? ?
s[--i]
= '\O';
Imagine that you have just written this loop. Now simulate it mentally as it reads a line. The first boundary to test is the simplest: an empty line. If you start with a line that contains only a single newline, it's easy to see that the loop stops on the first iteration with i set to zero, so the last line decrements i to -1 and thus writes a null byte into s [-I], which is before the beginning of the array. Boundary condition testing finds the error. If we rewrite the loop to use the conventional idiom for filling an array with input characters, it looks like this: ? ?
.? ?
f o r (i= 0; i < MAX-1; i++) i f ((s[i] = getchar()) == '\n') break; s[i] = '\O';
Repeating the original boundary test, it's easy to verify that a line with just a newline is handled correctly: i is zero, the first input character breaks out of the loop. and
TEST AS YOU WRITE THE CODE
SECTION 6.1
141
' \ O ' is stored in s[O]. Similar checking for inputs of one and two characters followed by a newline give us confidence that the loop works near that boundary. There are other boundary conditions to check, though. If the input contains a long line or no newlines, that is protected by the check that i stays less than MAX-1. But what if the input is empty, so the first call to getchar returns EOF? We must check for that: ? ? ? ?
f o r ( i = O ; i < M A X - 1 ; i++) if ( ( s [ i ] = getchar()) == ' \ n ' break; s [ i ] = '\O';
I I sCi1
== EOF)
Boundary condition testing can catch lots of bugs, but not all of them. We will return to this example in Chapter 8, where we will show that it still has a portability bug. The next step is to check input at the other boundary, where the array is nearly full, exactly full, and over-full, particularly if the newline arrives at the same time. We won't write out the details here, but it's a good exercise. Thinking about the boundaries raises the question of what to d o when the buffer fills before a '\n' occurs; this gap in the specification should be resolved early, and testing boundaries helps to identify it. Boundary condition checking is effective for finding off-by-one errors. With practice, it becomes second nature, and many trivial bugs are eliminated before they ever happen.
Test pre- and post-conditions. Another way to head off problems is to verify that expected or necessary properties hold before (pre-condition) and after (post-condition) some piece of code executes. Making sure that input values are within range is a common example of testing a pre-condition. This function for computing the average of n elements in an array has a problem if n is less than or equal to zero: double avg(doub1e a [ ] , i n t n)
C i n t i; double sum; sum = 0 . 0 ; f o r (i = 0 ; i < n; i++) sum += a [ i l ; r e t u r n sum / n;
3 What should avg do if n is zero? An array with no elements is a meaningful concept although its average value is not. Should avg let the system catch the division by zero? Abort? Complain'? Quietly return some innocuous value? What if n is negative, which is nonsensical but not impossible? As suggested in Chapter 4, our preference would probably be to return 0 as the average if n is less than or equal to zero: r e t u r n n 0 , f i l e a v g t e s t - c , l i n e 7 Abort(crash) Assertions are particularly helpful for validating properties of interfaces because they draw attention to inconsistencies between caller and callee and may even indicate who's at fault. If the assertion that n is greater than zero fails when the function is called, it points the finger at the caller rather than at avg itself as the source of trouble. If an interface changes but we forget to fix some routine that depends on it, an assertion may catch the mistake before it causes real trouble.
Program defensively. A useful technique is to add code to handle "can't happen" cases, situations where it is not logically possible for something to happen but (because of some failure elsewhere) it might anyway. Adding a test for zero or negative array lengths to avg was one example. As another example, a program processing grades might expect that there would be no negative or huge values but should check anyway: i f (grade < 0 1 I grade > 100) l e t t e r = '?' ; e l s e i f (grade >= 90) l e t t e r = 'A' ; else
/* c a n ' t happen */
...
This is an example of defensive progrtrmming: making sure that a program protects itself against incorrect use or illegal data. Null pointers, out of range subscripts, division by zero, and other errors can be detected early and warned about or deflected. Defensive programming (no pun intended) might well have caught the zero-divide problem on the Yorktown.
TEST AS YOU WRITE THE CODE
SECTION 6.1
143
Check error returns. One often-overlooked defense is to check the error returns from library functions and system calls. Return values from input routines such as f read and fscanf should always be checked for errors, as should any file open call such as fopen. If a read or open fails, computation cannot proceed correctly. Checking the return code from output functions like f p r i n t f or fwri t e will catch the error that results from trying to write a file when there is no space left on the disk. It may be sufficient to check the return value from fclose, which returns EOF if any error occurred during any operation, and zero otherwise. f p = f o p e n ( o u t f i l e , "w"); /a w r i t e output t o o u t f i l e while (...) f p r i n t f ( f p , ... ) ; i f (fclose(fp) == EOF) { /a any e r r o r s ? a/ /a some output e r r o r occurred */
*/
Output errors can be serious. If the file being written is the new version of a precious file, this check will save you from removing the old file if the new one was not wntten successfully. The effort of testing as you go is minimal and pays off handsomely. Thinking about testing as you write a program will lead to better code, because that's when you know best what the code should do. If instead you wait until something breaks, you will probably have forgotten how the code works. Working under pressure, you will need to figure it out again, which takes time, and the fixes will be less thorough and more fragile because your refreshed understanding is likely to be incomplete.
Exercise 6-1. Check out these examples at their boundaries, then fix them as necessary according to the principles of style in Chapter I and the advice in this chapter. (a) This is supposed to compute factorials: ?
i n t f a c t o r i a1 (i n t n)
? ?
{
i n t fac; f a c = 1; while (n--1 f a c a= n; return f a c ;
?
? ?
? ?
I
(b) This is supposed to print the characters of a string one per line: ? ?
? ? ?
i=O; do { putcharcs Ci++l); putchar('\nl); ) while (s[i] != ' \ 0 ' ) ;
144
CHAPTER 8
TESTING
(c) This is meant to copy a string from source to destination: ?
v o i d strcpy(char adest, char asrc)
?
{
i n t i;
? ? ?
f o r (i= 0; s r c [ i ] != ' \ O ' ; dest[i] = src[i];
? ?
i++)
3
(d) Another string copy, which attempts to copy n characters from s to t: v o i d strncpy(char
at,
char as, i n t n)
{
w h i l e (n > 0 && as != ' \ O ' ) a t = as; t++ ; s++ ; n-- ;
{
1 3 (e) A numerical comparison: ? ?
? ?
i f (i> j) p r i n t f ( " % d i s greater than %d.\nW, i, j); else p r i n t f ( " % d i s smaller than %d.\n", i , j);
(0A character class test: ?
if
? ? ? ? ?
>= ' A ' i f (c out1 new-ka $i>out2 i f ! cmp -s out1 out2 then echo $i: BAD fi done
#
loop over t e s t data f i l e s
# # #
run the old version run the new version compare output f i l e s
#
different: print error message
A test script should usually run silently, producing output only if something unexpected occurs, as this one does. We could instead choose to print each file name as it is being tested, and to follow it with an error message if something goes wrong. Such indications of progress help to identify problems like an infinite loop or a test script that is failing to run the right tests, but the extra chatter is annoying if the tests are running properly.
150
TESTING
CHAPTER 6
The -s argument causes cmp to report status but produce no output. If the files compare equal, cmp returns a true status, ! cmp is false, and nothing is printed. If the old and new outputs differ. however, cmp returns false and the file name and a warning are printed. There is an implicit assumption in regression testing that the previous version of the program computes the right answer. This must be carefully checked at the beginning of time, and the invariant scrupulously maintained. If an erroneous answer ever sneaks into a regression test, it's very hard to detect and everything that depends on it will be wrong thereafter. It's good practice to check the regression test itself periodically to make sure it is still valid.
Create self-contained tests. Self-contained tests that carry their own inputs and expected outputs provide a complement to regression tests. Our experience testing Awk may be instructive. Many language constructions are tested by running specified inputs through tiny programs and checking that the right output is produced. The following part of a large collection of miscellaneous tests verifies one tricky increment expression. This test runs the new version of Awk (newawk) on a short Awk program to produce output in one file, writes the correct output to another file with echo, compares the files, and reports an error if they differ. # f i e l d increment t e s t : $ i + + means ($i)++,
echo 3 5
1 newawk ' { i
echo ' 3 4 1' >out2
not $(i++)
= 1; p r i n t $ i + + ; p r i n t $1,
i } ' >out1
# c o r r e c t answer
i f ! cmp -s o u t 1 out2 # outputs a r e d i f f e r e n t then echo 'BAD: f i e l d increment t e s t f a i l e d ' fi
The first comment is part of the test input; it documents what the test is testing. Sometimes it is possible to construct a large number of tests with modest effort. For simple expressions. we created a small. specialized language for describing tests, input data, and expected outputs. Here is a short sequence that tests some of the ways that the numeric value 1 can be represented in Awk: t r y €if ( $ 1 == 1) p r i n t "yes"; e l s e p r i n t "no"} 1 Yes 1.0 yes 1EO yes 0.1E1 yes 10E-1 yes Yes 01 +1 Yes 10E-2 no 10 no
SECTION 6.4
TEST SCAFFOLDS
151
The first line is a program to be tested (everything after the word try). Each subsequent line is a set of inputs and the expected output, separated by tabs. The first test says that if the first input field is 1 the output should be yes. The first seven tests should all print yes and the last two tests should print no. An Awk program (what else?) converts each test into a complete Awk program, then runs each input through it, and compares actual output to expected output; it reports only those cases where the answer is wrong. Similar mechanisms are used to test the regular expression matching and substitution commands. A little language for writing tests makes it easy to create a lot of them; using a program to write a program to test a program has high leverage. (Chapter 9 has more to say about little languages and the use of programs that write programs.) Overall, there are about a thousand tests for Awk; the whole set can be run with a single command. and if everything goes well, no output is produced. Whenever a feature is added or a bug is fixed, new tests are added to verify correct operation. Whenever the program is changed, even in a trivial way, the whole test suite is run; it takes only a few minutes. It sometimes catches completely unexpected errors, and has saved the authors of Awk from public embarrassment many times. What should you do when you discover an error? If it was not found by an existing test, create a new test that does uncover the problen~and verify the test by running it with the broken version of the code. The error may suggest further tests or a whole new class of things to check. Or perhaps it is possible to add defenses to the program that would catch the error internally. Never throw away a test. It can help you decide whether a bug report is valid or describes something already fixed. Keep a record of bugs, changes, and fixes; it will help you identify old problems and fix new ones. In most commercial programming shops. such records are mandatory. For your personal programming, they are a small investment that will pay off repeatedly.
Exercise 6-5. Design a test suite for p r i ntf, using as many mechanical aids as possible.
6.4 Test Scaffolds Our discussion so far is based largely on testing a single stand-alone program in its completed form. This is not the only kind of test automation. however, nor is it the most likely way to test parts of a big program during construction, especially if you are part of a team. Nor is it the most effective way to test small components that are buried in something larger. To test a component in isolation, it's usually necessary to create some kind of framework or scaffold that provides enough support and interface to the rest of the
152
TESTING
CHAPTER 6
system that the part under test will run. We showed a tiny example for testing binary search earlier in this chapter. It's easy to build scaffolds for testing mathematical functions, string functions, sort routines, and so on, since the scaffolding is likely to consist mostly of setting up input parameters, calling the functions to be tested, then checking the results. It's a bigger job to create scaffolding for testing a partly-completed program. To illustrate, we'll walk through building a test for memset, one of the mem.. . functions in the C/C++ standard library. These functions are often written in assembly language for a specific machine, since their performance is important. The more carefully tuned they are, however, the more likely they are to be wrong and thus the more thoroughly they should be tested. The first step is to provide the simplest possible C versions that are known to work; these provide a benchmark for performance and, more important, for correctness. To move to a new environment, one carries the simple versions and uses them until the tuned ones are working. The function memset (s, c , n) sets n bytes of memory to the byte c, starting at address s, and returns s. This function is easy if speed is not an issue: /+ memset: s e t f i r s t n bytes o f s to c void *memset(void +s, i n t c , size- t n)
*/
s i z e - t i; char +p; p = (char +) s; f o r (i= 0 ; i < n; i++) p[i] = c; r e t u r n s;
1 But when speed is an issue, tricks like writing full words of 32 or 64 bits at a time are used. These can lead to bugs, so extensive testing is mandatory. Testing is based on a combination of exhaustive and boundary-condition checks at likely points of failure. For memset, the boundaries include obvious values of n such as zero, one and two, but also values that are powers of two or nearby values. including both small ones and large ones like 216, which corresponds to a natural boundary in many machines, a 16-bit word. Powers of two deserve attention because one way to make memset faster is to set multiple bytes at one time; this might be done by special instructions or by trying to store a word at a time instead of a byte. Similarly, we want to check array origins with a variety of alignments in case there is some error based on starting address or length. We will place the target array inside a larger array, thus creating a buffer zone or safety margin on each side and giving us an easy way to vary the alignment. We also want to check a variety of values for c, including zero, Ox7F (the largest signed value, assuming 8-bit bytes), 0x80 and OxFF (probing at potential errors involving signed and unsigned characters), and some values much bigger than one
SECTION 6.4
TEST SCAFFOLDS
153
byte (to be sure that only one byte is used). We should also initialize memory to some known pattern that is different from any of these character values so we can check whether memset wrote outside the valid area. We can use the simple implementation as a standard of comparison in a test that allocates two arrays, then compares behaviors on combinations of n, c and offset within the array: big
=
maximum l e f t margin + maximum n + maximum right margin
SO = ma1 loc(bi g)
s l = ma1 loc(bi g) f o r each combination of t e s t parameters n, c , and offset: set a l l of SO and s l t o known pattern run slow memset(s0 + o f f s e t , c , n) run f a s t memset(s1 + o f f s e t , c , n) check return values compare a l l of SO and sl byte by byte An error that causes rnemset to write outside the limits of its array is most likely to affect bytes near the beginning or the end of the array, so leaving a buffer zone makes it easier to see damaged bytes and makes it less likely that an error will overwrite some other part of the program. To check for writing out of bounds, we compare all the bytes of SO and s l , not just the n bytes that should be written. Thus a reasonable set of tests might include all combinations of: offset = 10, 11, ..., 20 c = 0, 1 , Ox7F, 0x80, OxFF, Ox11223344 n=0,1,2,3,4,5,7,8,9,15,16,17,
31, 32, 33,
..., 65535,
65536, 65537
The values of n would include at least 2' - l,2' and 2' + 1 for i from 0 to 16. These values should not be wired into the main pan of the test scaffold. but should appear in arrays that might be created by hand or by program. Generating them automatically is better; that makes it easy to specify more powers of two or to include more offsets and more characters. These tests will give memset a thorough workout yet cost very little time even to create, let alone run, since there are fewer than 3500 cases for the values above. The tests are completely portable, so they can be carried to a new environment as necessary. As a warning, consider this story. We once gave a copy of a memset tester to someone developing an operating system and libraries for a new processor. Months later, we (the authors of the original test) started using the machine and had a large application fail its test suite. We traced the problem to a subtle bug involving sign extension in the assembly language implementation of memset. For reasons unknown. the library implementer had changed the memset tester so it did not check values of c above Ox7F. Of course, the bug was isolated by running the original. working tester, once we realized that rnemset was a suspect.
154
TESTING
CHAPTER 6
Functions like memset are susceptible to exhaustive tests because they are simple enough that one can prove that the test cases exercise all possible execution paths through the code, thus giving complete coverage. For example, it is possible to test memmove for all combinations of overlap, direction, and alignment. This is not exhaustive in the sense of testing all possible copy operations, but it is an exhaustive test of representatives of each kind of distinct input situation. As in any testing method, test scaffolds need the correct answer to verify the operations they are testing. An important technique, which we used in testing memset, is to compare a simple version that is believed correct against a new version that may be incorrect. This can be done in stages, as the following example shows. One of the authors implemented a raster graphics library involving an operator that copied blocks of pixels from one image to another. Depending on the parameters, the operation could be a simple memory copy, or it could require converting pixel values from one color space to another, or it could require "tiling" where the input was copied repeatedly throughout a rectangular area, or combinations of these and other features. The specification of the operator was simple, but an efficient implementation would require lots of special code for the many cases. To make sure all that code was right demanded a sound testing strategy. First. simple code was written by hand to perform the correct operation for a single pixel. This was used to test the library version's handling of a single pixel. Once this stage was working, the library could be trusted for single-pixel operations. Next, hand-written code used the library a pixel at a time to build a very slow version of the operator that worked on a single horizontal row of pixels, and that was compared with the library's much more efficient handling of a row. With that working, the library could be trusted for horizontal lines. This sequence continued, using lines to build rectangles, rectangles to build tiles, and so on. Along the way, many bugs were found, including some in the tester itself, but that's part of the effectiveness of the method: we were testing two independent implementations, building confidence in both as we went. If a test failed, the tester printed out a detailed analysis to aid understanding what went wrong, and also to verify that the tester was working properly itself. As the library was modified and ported over the years, the tester repeatedly proved invaluable for finding bugs. Because of its layer-by-layer approach, this tester needed to be run from scratch each time, to verify its own trust of the library. Incidentally, the tester was not exhaustive, but probabilistic: it generated random test cases which, for long enough runs, would eventually explore every cranny of the code. With the huge number of possible test cases, this strategy was more effective than trying to construct a thorough test set by hand, and much more efficient than exhaustive testing.
Exercise 6-6. Create the test scaffold for memset along the lines that we indicated. Exercise 6-7. Create tests for the rest of the mem. . . family.
SECTION 6.5
STRESS TESTS
155
Exercise 6-8. Specify a testing regime for numerical routines like sqrt, sin, and so on, as found in math. h. What input values make sense? What independent checks can be performed? Exercise 6-9. Define mechanisms for testing the functions of the C s t r . . . family, like strcmp. Some of these functions, especially tokenizers like s t r t o k and strcspn, are significantly more complicated than the mem.. . family, so more sophisticated tests will be called for.
6.5 Stress Tests High volumes of machine-generated input are another effective testing technique. Machine-generated input stresses programs differently than input written by people does. Higher volume in itself tends to break things because very large inputs cause overflow of input buffers, arrays, and counters. and are effective at finding unchecked fixed-size storage within a program. People tend to avoid "impossible" cases like empty inputs or input that is out of order or out of range, and are unlikely to create very long names or huge data values. Computers, by contrast, produce output strictly according to their programs and have no idea of what to avoid. To illustrate. here is a single line of output produced by the Microsoft Visual C++ Version 5.0 compiler while compiling the C++ STL implementation of markov; we have edited the line so it fits: x t r e e ( l l 4 ) : warning C4786: 'std::-Treecstd::deque<std:: basic- stri ngcchar, std: : char- trai tscchar>, std: : a1locator
~char>>,std::allocator~std::basic~string~char,std:: ... 1420 characters omitted a1 locator>>>>>: :it e r a t o r ' : i d e n t i f i e r was truncated t o ' 2 5 5 ' characters i n the debug information
The compiler is warning us that it has generated a variable name that is a remarkable 1594 characters long but that only 255 characters have been preserved as debugging information. Not all programs defend themselves against such unusually long strings. Random inputs (not necessarily legal) are another way to assault a program in the hope of breaking something. This is a logical extension of "people don't do that" reasoning. For example, some commercial C compilers are tested with randomlygenerated but syntactically valid programs. The trick is to use the specification of the problem-in this case, the C standard-to drive a program that produces valid but bizarre test data. Such tests rely on detection by built-in checks and defenses in the program, since it may not be possible to verify that the program is producing the right output; the goal is more to provoke a crash or a "can't happen" than to uncover straightforward errors. It's also a good way to test that error-handling code works. With sensible input, most errors don't happen and code to handle them doesn't get exercised: by
156
TESTING
CHAPTER 6
nature, bugs tend to hide in such comers. At some point, though, this kind of testing reaches diminishing returns: it finds problems that are so unlikely to happen in real life they may not be worth fixing. Some testing is based on explicitly malicious inputs. Security attacks often use big or illegal inputs that overwrite precious data; it is wise to look for such weak spots. A few standard library functions are vulnerable to this sort of attack. For instance, the standard library function g e t s provides no way to limit the size of an input line, so it should never be used; always use f g e t s ( b u f , s i z e o f (buf) , s t d i n ) instead. A bare scanf ("%sM,buf) doesn't limit the length of an input line either; it should therefore usually be used with an explicit length, such as scanf ("%20sW,buf). In Section 3.3 we showed how to address this problem for a general buffer size. Any routine that might receive values from outside the program, directly or indirectly, should validate its input values before using them. The following program from a textbook is supposed to read an integer typed by a user, and warn if the integer is too long. Its goal is to demonstrate how to overcome the g e t s problem, but the solution doesn't always work. # d e f i n e MAXNUM 1 0
in t mai n (voi d)
C char num[MAXNUM] ; memset(num, 0 , sizeof(num)); p r i n t f ( " T y p e a number: ") ; gets bum) ; if (num [MAXNUM-l] ! = 0) p r i n t f ("Number t o o b i g .\nW) ;
/*
...
*/
1 If the input number is ten digits long, it will overwrite the last zero in array num with a non-zero value, and in theory this will be detected after the return from gets. Unfortunately, this is not sufficient. A malicious attacker can provide an even longer input string that overwrites some critical value, perhaps the return address for the call, so the program never returns to the i f statement but instead executes something nefarious. Thus this kind of unchecked input is a potential security problem. Lest you think that this is an irrelevant textbook example, in July, 1998 an error of this form was uncovered in several major electronic mail programs. As the New York Times reported, The security hole is caused by what is known as a "buffer overflow error." Programmers are supposed to include code in their software to check that incoming data are of a safe type and that the units are arriving at the right length. If a unit of data is too long, it can overrun the "buffer"-the chunk of memory set aside to hold it. In that case, the E-mail program will crash, and a hostile programmer can trick the computer into running a malicious program in its place.
SECTION 6.5
STRESS TESTS
157
This was also one of the attacks in the famous "Internet Worm" incident of 1988. Programs that parse HTML forms can also be vulnerable to attacks that store very long input strings in small arrays: s t a t i c char query [lo241 ; char *read-form(void)
C i n t qsize;
qsi ze = a t o i (getenv("C0NTENT-LENGTH")) fread(query, q s i z e . 1 , s t d i n ) ; return query;
;
1 The code assumes that the input will never be more than 1024 bytes long so, like gets. it is open to an attack that overflows its buffer. More familiar kinds of overflow can cause trouble, too. If integers ovefflow silently, the result can be disastrous. Consider an allocation like ? ?
char *p; p=(char*)malloc(x*y*z);
If the product of x, y, and z overflows, the call to malloc might produce a reasonable-sized array, but p [XI might refer to memory outside the allocated region. Suppose that i n t s are 16 bits and x. y, and z are each 41. Then x*y*z is 68921, which is 3385 modulo 2'" So the call to ma1 l o c allocates only 3385 bytes; any reference with a subscript beyond that value will be out of bounds. Conversion between types is another source of ovefflow, and catching the error may not be good enough. The Ariane 5 rocket exploded on its maiden flight in June, 1996 because the navigation package was inherited from the Ariane 4 without proper testing. The new rocket flew faster, resulting in larger values of some variables in the navigation software. Shortly after launch, an attempt to convert a 64-bit floatingpoint number into a 16-bit signed integer generated an overflow. The error was caught, but the code that caught it elected to shut down the subsystem. The rocket veered off course and exploded. It was unfortunate that the code that failed generated inertial reference information useful only before lift-off; had it been turned off at the moment of launch. there would have been no trouble. On a more mundane level, binary inputs sometimes break programs that expect text inputs, especially if they assume that the input is in the 7-bit ASCII character set. It is instructive and sometimes sobering to pass binary input (such as a compiled program) to an unsuspecting program that expects text input. Good test cases can often be used on a variety of programs. For example, any program that reads files should be tested on an empty file. Any program that reads text should be tested on binary files. Any program that reads text lines should be tested on huge lines and empty lines and input with no newlines at all. It's a good idea to keep
158
TESTING
CHAPTER 6
a collection of such test files handy, so you can test any program with them without having to recreate the tests. Or write a program to create test files upon demand. When Steve Bourne was writing his Unix shell (which came to be known as the Bourne shell), he made a directory of 254 files with one-character names, one for each byte value except '\0' and slash, the two characters that cannot appear in Unix file names. He used that directory for all manner of tests of pattern-matching and tokenization. (The test directory was of course created by a program.) For years afterwards, that directory was the bane of file-tree-walking programs; it tested them to destruction.
Exercise 6-10. Try to create a file that will crash your favorite text editor, compiler, or other program.
6.6 Tips for Testing Experienced testers use many tricks and techniques to make their work more productive; this section includes some of our favorites. Programs should check array bounds (if the language doesn't do it for them), but the checking code might not be tested if the array sizes are large compared to typical input. To exercise the checks, temporarily make the array sizes very small, which is easier than creating large test cases. We used a related trick in the array-growing code in Chapter 2 and in the CSV library in Chapter 4. In fact. we left the tiny initial values in place, since the additional startup cost is negligible. Make the hash function return a constant, so every elemen1 gets installed in the same hash bucket. This will exercise the chaining mechanism; it also provides an indication of worst-case performance. Write a version of your storage allocator that intentionally fails early, to test your code for recovering from out-of-memory errors. This version returns NULL after 10 calls:
/* testmalloc: returns NULL a f t e r 10 c a l l s v o i d *testma1 1oc(si ze-t n)
*/
C s t a t i c i n t count = 0; i f (++count > 10) r e t u r n NULL; else r e t u r n ma1 1oc(n) ;
1 Before you ship your code. disable testing limitations that will affect performance. We once tracked down a performance problem in a production compiler to a hash function that always returned zero because testing code had been left installed.
SECTION 6.7
WHO DOES THE TESTING?
159
Initialize arrays and variables with some distinctive value, rather than the usual default of zero; then if you access out of bounds or pick up an uninitialized variable, you are more likely to notice it. The constant OxDEADBEEF is easy to recognize in a debugger; allocators sometimes use such values to help catch uninitialized data. Vary your test cases, especially when making small tests by hand-it's easy to get into a rut by always testing the same thing, and you may not notice that something else has broken. Don't keep on implementing new features or even testing existing ones if there are known bugs; they could be affecting the test results. Test output should include all input parameter settings, so the tests can be reproduced exactly. If your program uses random numbers, have a way to set and print the starting seed, independent of whether the tests themselves are random. Make sure that test inputs and corresponding outputs are properly identified, so they can be understood and reproduced. It's also wise to provide ways to make the amount and type of output controllable when a program is run; extra output can help during testing. Test on multiple machines, compilers, and operating systems. Each combination potentially reveals errors that won't be seen on others, such as dependencies on byteorder, sizes of integers, treatment of null pointers. handling of carriage return and newline, and specific properties of libraries and header files. Testing on multiple machines also uncovers problems in gathering the components of a program for shipment and, as we will discuss in Chapter 8, may reveal unwitting dependencies on the development environment. We will discuss performance testing in Chapter 7.
6.7 Who Does the Testing? Testing that is done by the implementer or someone else with access to the source code is sometimes called white box testing. (The term is a weak analogy to black box testing, where the tester does not know how the component is implemented; "clear box" might be more evocative.) It is important to test your own code: don't assume that some testing organization or user will find things for you. But it's easy to delude yourself about how carefully you are testing, so try to ignore the code and think of hard cases, not easy ones. To quote Don Knuth describing how he creates tests for the TEX formatter, "I get into the meanest, nastiest frame of mind that I can manage, and I write the nastiest [testing] code I can think of; then I turn around and embed that in even nastier constructions that are almost obscene." The reason for testing is to find bugs, not to declare the program working. Therefore the tests should be tough, and when they find problems, that is a vindication of your methods, not a cause for alarm. Black box testing means that the tester has no knowledge of or access to the innards of the code. It finds different kinds of errors, because the tester has different assumptions about where to look. Boundary conditions are a good place to begin
160
TESTING
CHAPTER 6
black box testing; high-volume, perverse, and illegal inputs are good follow-ons. Of course you should also test the ordinary "middle of the road" or conventional uses of the program to verify basic functionality. Real users are the next step. New users find new bugs, because they probe the program in unexpected ways. It is important to do this kind of testing before the program is released to the world though, sadly, many programs are shipped without enough testing of any kind. Beta releases of software are an attempt to have numerous real users test a program before it is finalized, but beta releases should not be used as a substitute for thorough testing. As software systems get larger and more complex, and development schedules get shorter, however, the pressure to ship without adequate testing increases. It's hard to test interactive programs, especially if they involve mouse input. Some testing can be done by scripts (whose properties depend on language, environment, and the like). Interactive programs should be controllable from scripts that simulate user behaviors so they can be tested by programs. One technique is to capture the actions of real users and replay them; another is to create a scripting language that describes sequences and timing of events. Finally, give some thought to how to test the tests themselves. We mentioned in Chapter 5 the confusion caused by a faulty test program for a list package. A regression suite infected by an error will cause trouble for the rest of time. The results of a set of tests will not mean much if the tests themselves are flawed.
6.8 Testing the Markov Program The Markov program of Chapter 3 is sufficiently intricate that it needs careful testing. It produces nonsense, which is hard to analyze for validity, and we wrote multiple versions in several languages. As a final complication, its output is random and different each time. How can we apply some of the lessons of this chapter to testing this program? The first set of tests consists of a handful of tiny files that check boundary conditions, to make sure the program produces the right output for inputs that contain only a few words. For prefixes of length two, we use five files that contain respectively (with one word per line)
For each file, the output should be identical to the input. These checks uncovered several off-by-one errors in initializing the table and starting and stopping the generator.
TESTING THE MARKOV PROGRAM
SECTION 6.8
161
A second test verified conservation properties. For two-word prefixes, every word, every pair, and every triple that appears in the output of a run must occur in the input as well. We wrote an Awk program that reads the original input into a giant array. builds arrays of all pairs and triples, then reads the Markov output into another array and compares the two: # markov t e s t : check t h a t a l l words, p a i r s , t r i p l e s i n # output ARGV[2] are i n o r i g i n a l i n p u t ARGV[l] BEGIN {
while (get1 ine iARGV[l] > 0) { f o r (i= 1; i = NSTART) epri n t f ("too many p a t t e r n s (>=%d) begin '%c"' , NSTART, c ) ; s t a r t i n g [ c ] [nstarting[c]++] = i ; patlen[i] = s t r l e n ( p a t [ i ] ) ;
3 Depending on the input, the spam filter is now five to ten times faster than it was using the improved s t r s t r , and seven to fifteen times faster than the original implementation. We didn't get a factor of 52, partly because of the non-uniform distribution of letters, partly because the loop is more complicated in the new program, and partly because there are still many failing string comparisons to execute, but the spam filter is no longer the bottleneck for mail delivery. Performance problem solved. The rest of this chapter will explore the techniques used to discover performance problems, isolate the slow code. and speed it up. Before moving on, though, it's worth looking back at the spam filter to see what lessons it teaches. Most important, make sure performance matters. It wouldn't have been worth all the effort if spam filtering wasn't a bottleneck. Once we knew it was a problem, we used profiling and other techniques to study the behavior and learn where the problem really lay. Then we made sure we were solving the right problem, examining the overall program rather than just focusing on s t r s t r , the obvious but incorrect suspect. Finally, we solved the correct problem using a better algorithm, and checked that it really was faster. Once it was fast enough, we stopped; why over-engineer?
TIMING AND PROFILING
SECTION 7.2
171
Exercise 7-1. A table that maps a single character to the set of patterns that begin with that character gives an order of magnitude improvement. Implement a version of i sspam that uses two characters as the index. How much improvement does that lead to? Thcsc arc simple special cases of a data structure called a trie. Most such data
structures are based on trading space for time.
7.2 Timing and Profiling Automate timing measurements. Most systems have a command to measure how long a program takes. On Unix. the command is called time: % time slowprogram
real user
7.0 6.2 0.1
SYS
%
This runs the command and reports three numbers, all in seconds: "real" time, the elapsed time for the program to complete; "user" CPU time. time spent executing the user's program; and "system" CPU time, time spent within the operating system on the program's behalf. If your system has a similar command, use it; the numbers will be more informative, reliable, and easier to track than time measured with a stopwatch. And keep good notes. As you work on the program, making modifications and measurements, you will accumulate a lot of data that can become confusing a day or two later. (Which version was it that ran 20% faster?) Many of the techniques we discussed in the chapter on testing can be adapted for measuring and improving performance. Use the machine to run and measure your test suites and, most inlportant, use regression testing to make sure your modifications don't break the program. If your system doesn't have a time command, or if you're timing a function in isolation, it's easy to construct a timing scaffold analogous to a testing scaffold. C and C++ provide a standard routine, clock, that reports how much CPU time the program has consumed so far. It can be called before and after a function to measure CPU usage: #i ncl ude #include < s t d io. h>
...
clock-t before; doubl e elapsed; before = clock(); long-runni ng-function0 ; elapsed = clock() - before; p r i n t f ( " f u n c t i o n used %.3f seconds\nN, e l apsed/CLOCKS-PER-SEC) ;
172
PERFORMANCE
CHAPTER 7
The scaling term, CLOCKS-PER-SEC, records the resolution of the timer as reported by clock. If the function takes only a small fraction of a second, run it in a loop. but be sure to compensate for loop overhead if that is significant: before = clock(); f o r ( i = 0 ; i < 1000; i++) short- runni ng-function() ; elapsed = (clock()-before)/(double)i
;
In Java, functions in the Date class give wall clock time, which is an approximation to CPU time:
Date before = new Date(); long-runni ng-function() ; Date a f t e r = new Date(); long elapsed = a f t e r . g e t T i m e 0 - before.getTime(); The return value of getTime is in milliseconds.
Use a profler. Besides a reliable timing method, the most important tool for performance analysis is a system for generating profiles. A prqfile is a measurement of where a program spends its time. Some profiles list each function, the number of times it is called, and the fraction of execution time it consumes. Others show counts of how many times each statement was executed. Statements that are executed frequently contribute more to run-time, while statements that are never executed may indicate useless code or code that is not being tested adequately. Profiling is an effective tool for finding hot spots in a program, the functions or sections of code that consume most of the computing time. Profiles should be interpreted with care, however. Given the sophistication of compilers and the complexity of caching and memory effects. as well as the fact that profiling a program affects its performance, the statistics in a profile can be only approximate. In the 1971 paper that introduced the term profiling, Don Knuth wrote that "less than 4 per cent of a program generally accounts for more than half of its running time." This indicates that the way to use profiling is to identify the critical timeconsuming parts of the program, improve them to the degree possible, and then measure again to see if a new hot spot has surfaced. Eventually, often after only one or two iterations. there is no obvious hot spot left. Profiling is usually enabled with a special compiler flag or option. The program is run, and then an analysis tool shows the results. On Unix, the flag is usually -p and the tool is called prof: % cc -p s p a m t e s t . ~-0 spamtest % spamtest % prof spamtest
The following table shows the profile generated by a special version of the spam filter we built to understand its behavior. It uses a fixed message and a fixed set of 217 phrases, which it matches against the message 10,000 times. This run on a 250 MHz
SECTION 7.2
TIMING AND PROFILING
173
MIPS R 10000 used the original implementation of s t r s t r that calls other standard functions. The output has been edited and reformatted so it fits the page. Notice how sizes of input (217 phrases) and the number of runs (10.000) show up as consistency checks in the "calls" column, which counts the number of calls of each function.
12234768552: Total number of instructions executed 13961810001: Total computed cycles 55.847: Total computed execution time (secs.) 1.141: Average cycles I instruction secs 45.260
%
81.0%
cum% 81.O%
cycles 1 1314990000
instructions 9440110000
calls 48350000
function strchr strncmp strstr strlen isspam main -memccpy strcpy fgets -malloc realfree estrdup cleanfree readpat getline -malloc
It's obvious that s t r c h r and strncmp, both called by s t r s t r , completely dominate the performance. Knuth's guideline is right: a small part of the program consumes most of the run-time. When a program is first profiled, it's common to see the top-running function at 50 percent or more, as it is here, making it easy to decide where to focus attention.
Concentrate on the hot spots. After rewriting s t r s t r , we profiled spamtest again and found that 99.8% of the time was now spent in s t r s t r alone. even though the whole program was considerably faster. When a single function is so overwhelmingly the bottleneck, there are only two ways to go: improve the function to use a better algorithm, or eliminate the function altogether by rewriting the surrounding program. In this case, we rewrote the program. Here are the first few lines of the profile for spamtest using the final, fast implementation of i sspam. Notice that the overall time is much less. that memcmp is now the hot spot, and that isspam now consumes a significant fraction of the computation. It is more complex than the version that called s t r s t r , but its cost is more than compensated for by eliminating s t r l e n and s t r c h r from isspam and by replacing strncmp with memcmp, which does less work per byte.
174
PERFORMANCE
secs 3.524 2.662 0.001 0.000
9% 56.9% 43.04 0.0% 0.0%
CHAPTER 7
cum% cvcles 56.9%. 880890000 100.04. 665550000 100.0% 140304 100.0% 100025
instructions I027590000 902920000 106043 100028
calls 46 180000 10000 652 1
function rnerncrnp issparn strlen main
It's instructive to spend some time comparing the cycle counts and number of calls in the two profiles. Notice that s t r l e n went from a couple of million calls to 652, and that strncmp and memcmp are called the same number of times. Also notice that isspam. which now incorporates the function of s t r c h r , still manages to use far fewer cycles than s t r c h r did before because it examines only the relevant patterns at each step. Many more details of the execution can be discovered by examining the numbers. A hot spot can often be eliminated, or at least cooled, by much simpler engineering than we undertook for the spam filter. Long ago, a profile of Awk indicated that one function was being called about a million times over the course of a regression test, in this loop: .?
'?
f o r ( j = i ;j<MAXFLD; j++) clear(j1;
The loop, which clears fields before each new input line is read, was taking as much as 50 percent of the run-time. The constant MAXFLD. the maximum number of fields permitted in an input line, was 200. But in most uses of Awk, the actual number of fields was only two or three. Thus an enormous amount of time was being wasted clearing fields that had never been set. Replacing the constant by the previous value of the maximum number of fields gave a 25 percent overall speedup. The fix was to change the upper limit of the loop: f o r ( j = i ; j < maxfld; j++) c l e a r ( j >; maxfld = i ; Draw a picture. Pictures are especially good for presenting performance measure-
ments. They can convey information about the effects of parameter changes, compare algorithms and data structures, and sometimes point to unexpected behavior. The graphs of chain length counts for several hash multipliers in Chapter 5 showed clearly that some multipliers were better than others. The following graph shows the effect of the size of the hash table array on runtime for the C version of markov with Psalms as input (42,685 words, 22,482 prefixes). We did two experiments. One set of runs used array sizes that are powers of two from 2 to 16.384; the other used sizes that are the largest prime less than each power of two. We wanted to see if a prime array size made any measurable difference to the performance.
STRATEGIES FOR SPEED
SECTION 7.3
5020 10 Run-time 5 -
(sec.)
175
x Power of two
H
Prime
x
k x '
210.5 -
R: K I
*
* x * * x
Hash Table Size The graph shows that run-time for this input is not sensitive to the table size once the size is above 1,000 elements, nor is there a discernible difference between prime and power-of-two table sizes.
Exercise 7-2. Whether or not your system has a t i m e com.nand, use c l o c k or getTime to write a timing facility for your own use. Compare its times to a wall clock. How does other activity on the machine affect the timings?
Exercise 7-3. In the first profile, s t r c h r was called 48,350,000 times and strncmp only 46,180,000. Explain the difference.
7.3 Strategies for Speed Before changing a program to make it faster, be certain that it really is too slow, and use timing tools and profilers to discover where the time is going. Once you know what's happening, there are a number of strategies to follow. We list a few here in decreasing order of profitability.
Use a better algorithm or data structure. The most important factor in making a program faster is the choice of algorithm and data structure; there can be a huge difference between an algorithm that is efficient and one that is not. Our spam filter saw a change in data structure that was worth a factor of ten; even greater improvement is possible if the new algorithm reduces the order of computation, say from 0 ( n 2 ) to O(nlogn). We covered this topic in Chapter 2, so we won't dwell on it here. Be sure that the complexity is really what you expect; if not, there might be a hidden performance bug. This apparently linear algorithm for scanning a string, ?
'? ?
f o r (i= 0; i < s t r l e n ( s ) ; i f ( s [ i ] == c)
...
i++)
176
CHAPTER 7
PERFORMANCE
is in fact quadratic: if s has n characters, each call to s t r l e n walks down the n characters of the string and the loop is perfarmed n times.
Enable compiler optimizations. One zero-cost change that usually produces a reasonable improvement is to turn on whatever optimization the compiler provides. Modem compilers do sufficiently well that they obviate much of the need for small-scale changes by programmers. By default, most C and C++ compilers do not attempt much optimization. A compiler option enables the optimizer ("improver" would be a more accurate term). It should probably be the default except that the optimizations tend to confuse sourcelevel debuggers, so programmers must enable the optimizer explicitly once they believe the program has been debugged. Compiler optimization usually improves run-time anywhere from a few percent to a factor of two. Sometimes, though, it slows the program down, so measure the improvement before shipping your product. We compared unoptimized and optimized compilation on a couple of versions of the spam filter. For the test suite using the final version of the matching algorithm, the original run-time was 8.1 seconds, which dropped to 5.9 seconds when optimization was enabled, an improvement of over 25%. On the other hand, the version that used the fixed-up s t r s t r showed no improvement under optimization, because s t r s t r had already been optimized when it was installed in the library; the optimizer applies only to the source code being compiled now and not to the system libraries. However, some compilers have global optim i z e r ~which , analyze the entire program for potential improvements. If such a compiler is available on your system, try it; it might squeeze out a few more cycles. One thing to be aware of is that the more aggressively the compiler optimizes, the more likely it is to introduce bugs into the compiled program. After enabling the optimizer, re-run your regression test suite. as you should for any other modification. Tune the code. The right choice of algorithm matters if data sizes are big enough. Furthermore, algorithmic improvements work across different machines, compilers and languages. But once the right algorithm is in place, if speed is still an issue the next thing to try is tuning the code: adjusting the details of loops and expressions to make things go faster. The version of isspam we showed at the end of Section 7.1 hadn't been tuned. Here, we'll show what further improvements can be achieved by tweaking the loop. As a reminder, this is how we left it: f o r ( j = 0 ; (c = mesg[j]) != '\O'; j++) ( f o r (i = 0 ; i < n s t a r t i n g [ c ] ; i++) 1 k = s t a r t i n g [ c ] [i]; i f (memcmp(mesg+j , p a t [k] , pat1en [k] ) == 0) ( p r i n t f ("spam: match f o r '%s'\n" , p a t [ k l ) ; r e t u r n 1;
1 1
1
SECTION 7.3
STRATEGIES FOR SPEED
177
This initial version takes 6.6 seconds in our test suite when compiled using the optimizer. The inner loop has an array index ( n s t a r t i ng [c]) in its loop condition whose value is fixed for each iteration of the outer loop. We can avoid recalculating it by saving the value in a local variable: f o r (j = 0; (C = mesg[jl) != '\O'; j++) { n = n s t a r t i ng [c] ; f o r (i = 0; i < n; i++) { k = s t a r t i n g C c 1 [i] ;
This drops the time to 5.9 seconds, about 10% faster, a speedup typical of what tuning can achieve. There's another variable we can pull out: s t a r t i ng[cl is also fixed. It seems like pulling that computation out of the loop would also help, but in our tests it made no measurable difference. This. too. is typical of tuning: some things help, some things don't. and one must measure to find out which. And results will vary with different machines or compilers. There is another change we could make to the spam filter. The inner loop compares the entire pattern against the string. but the algorithm ensures that the first character already matches. We can therefore tune the code to start memcmp one byte further along. We tried this and found it gave about 3% improvement, which is slight but it requires modifying only three lines of the program, one of them in precomputation.
Don't optimize what doesn't matter. Sometimes tuning achieves nothing because it is applied where it makes no difference. Make sure the code you're optimizing is where time is really spent. The following story might be apocryphal, but we'll tell it anyway. An early machine from a now-defunct company was analyzed with a hardware performance monitor and discovered to be spending 50 percent of its time executing the same sequence of several instructions. The engineers built a special instruction to encapsulate the function of the sequence, rebuilt the system, and found it made no difference at all; they had optimized the idle loop of the operating system. How much effort should you spend making a program run faster? The main criterion is whether the changes will yield enough to be worthwhile. As a guideline, the personal time spent making a program faster should not be more than the time the speedup will recover during the lifetime of the program. By this rule, the algorithmic improvement to i sspam was worthwhile: it took a day of work but saved (and continues to save) hours every day. Removing the array index from the inner loop was less dramatic, but still worth doing, since the program provides a service to a large community. Optimizing public services like the spam filter or a library is almost always worthwhile; speeding up test programs is almost never worthwhile. And for a program that runs for a year, squeeze out everything you can. It may be worth restarting if you find a way to make a ten percent improvement even after the program has been running for a month.
178
PERFORMANCE
CHAPTER 7
Competitive programs-games, compilers. word processors, spreadsheets, database systems-fall into this category as well, since commercial success is often to the swiftest, at least in published benchmark results. It's important to time programs as changes are being made, to make sure that things are improving. Sometimes two changes that each improve a program will interact, negating their individual effects. It's also the case that timing mechanisms can be so erratic that it's hard to draw firm conclusions about the effect of changes. Even on single-user systems. times can fluctuate unpredictably. If the variability of the internal timer (or at least what is reported back to you) is ten percent, changes that yield improvements of only ten percent are hard to distinguish from noise.
7.4 Tuning the Code There are many techniques to reduce run-time when a hot spot is found. Here are some suggestions. which should be applied with care. and with regression testing after each to be sure that the code still works. Bear in mind that good compilers will do some of these for you, and in fact you may impede their efforts by complicating the program. Whatever you try, measure its effect to make sure it helps.
Collect common subexpressions. If an expensive computation appears multiple times. do it in only one place and remember the result. For example. in Chapter 1 we showed a macro that computed a distance by calling s q r t twice in a row with the same values; in effect the computation was ?
sqrt(dxwdx
+
dywdy)
+
((sqrt(dxcdx
+
dywdy) > 0 ) ?
. . .)
Compute the square root once and use its value in two places. If a computation is done within a loop but does not depend on anything that changes within the loop. move the computation outside, as when we replaced f o r (i= 0 ; i < n s t a r t i n g [ c ] ;
i++) {
n = nstarting[c] ; f o r (i= 0; i < n; i++)(
Replace expensive operations by cheap ones. The term reduction in strerzgth refers to optimizations that replace an expensive operation by a cheaper one. In olden times, this used to mean replacing multiplications by additions or shifts. but that rarely buys much now. Division and remainder are much slower than multiplication. however, so there may be improvement if a division can be replaced with multiplication by the inverse, or a remainder by a masking operation if the divisor is a power of two. Replacing array indexing by pointers in C or C++ might speed things up, although most compilers do this automatically. Replacing a function call by a simpler calcula-
TUNING THE CODE
SECTION 7.4
179
tion can still be worthwhile. Distance in the plane is determined by the formula sqrt(dxadx+dyady). so to decide which of two points is further away would normally involve calculating two square roots. But the same decision can be made by comparing the squares of the distances;
gives the same result as comparing the square roots of the expressions. Another instance occurs in textual pattern matchers such as our spam filter or grep. If the pattern begins with a literal character, a quick search is made down the input text for that character; if no match is found, the more expensive search machinery is not invoked at all.
Unroll or eliminate loops. There is a certain overhead in setting up and running a loop. If the body of the loop isn't too long and doesn't iterate too many times. it can be more efficient to write out each iteration in sequence. Thus, for example. f o r (i = 0 ; i < 3; i++) a [ i l = b[i]
+
c[i];
becomes
This eliminates loop overhead, particularly branching, which can slow niodeni processors by interrupting the flow of execution. If the loop is longer, the same kind of transformation can be used to amortize the overhead over fewer iterations: f o r (i = 0 ; i < 3an; i++) a[i] = b[i]
+
c[i];
becomes
+= 3) { c[i+Ol; c[i+ll; c[i+21;
f o r (i = 0 ; i < 3an; i a [ i + O l = b[i+O] a[i+ll = b[i+l] a[i+21 = b[i+2]
+ + +
1 Note that this works only if the length is a multiple of the step size; otherwise additional code is needed to fix up the ends, which is a place for mistakes to creep in and for some of the efficiency to be lost again.
Cachefrequently-used values. Cached values don't have to be recomputed. Caching takes advantage of loccrlity, the tendency for programs (and people) to re-use recently accessed or nearby items in preference to older or distant data. Computing hardware makes extensive use of caches; indeed. adding cache memory to a computer can make
180
CHAPTER 7
PERFORMANCE
great improvements in how fast a machine appears. The same is true of software. Web browsers, for instance, cache pages and images to avoid the slow transfer of data over the Internet. In a print preview program we wrote years ago, non-alphabetic special characters like L/z had to be looked up in a table. Measurement showed that much of the use of special characters involved drawing lines with long sequences of the same single character. Caching just the single most recently used character made the program significantly faster on typical inputs. It's best if the caching operation is invisible from outside, so that it doesn't affect the rest of the program except for making it run faster. Thus in the case of the prinl previewer, the interface to the character drawing function didn't change; it was always drawchar (c) ; The original version of drawchar called show (1 ookup(c)). The cache implementation used internal static variables to remember the previous character and its code: if
!= l a s t c ) { / u update cache l a s t c = c; 1astcode = 1ookup(c) ;
(C
a/
1
show(1astcode) ;
Write a special-purpose allocator. Often the single hot spot in a program is memory allocation, which manifests itself as lots of calls on malloc or new. When most requests are for blocks of the same size, substantial speedups are possible by replacing calls to the general-purpose allocator by calls to a special-purpose one. The specialpurpose allocator makes one call to ma1 loc to fetch a big array of items, then hands them out one at a time as needed, a cheaper operation. Freed items are placed back in afree list so they can be reused quickly. If the requested sizes are similar, you can trade space for time by always allocating enough for the largest request. This can be effective for managing short strings if you use the same size for all strings up to a specified length. Some algorithms can use stack-based allocation, where a whole sequence of allocations is done, and then the entire set is freed at once. The allocator obtains one big chunk for itself and treats it as a stack, pushing allocated items on as needed and popping them all off in a single operation at the end. Some C libraries offer a function a11 oca for this kind of allocation, though it is not standard. It uses the local call stack as the source of memory, and frees all the items when the function that calls a l l o c a returns. Buffer input and output. Buffering batches transactions so that frequent operations are done with as little overhead as possible, and the high-overhead operations are done only when necessary. The cost of an operation is thereby spread over multiple data values. When a C program calls p r i n t f , for example, the characters are stored in a buffer but not passed to the operating system until the buffer is full or flushed explicitly. The operating system itself may in turn delay writing the data to disk. The
SECTION 7.4
TUNING THE CODE
181
drawback is the need to flush output buffers to make data visible; in the worst case, information still in a buffer will be lost if a program crashes.
Handle special cases separately. By handling same-sized objects in separate code, special-purpose allocators reduce time and space overhead in the general allocator and incidentally reduce fragmentation. In the graphics library for the Inferno system, the basic draw function was written to be as simple and straightforward as possible. With that working, optimizations for a variety of cases (chosen by profiling) were added one at a time; it was always possible to test the optimized version against the simple one. In the end, only a handful of cases were optimized because the dynamic distribution of calls to the drawing function was heavily skewed towards displaying characters; it wasn't worth writing clever code for all the cases. Precompute results. Sometimes it is possible to make a program run faster by precomputing values so they are ready when they are needed. We saw this in the spam filter, which precomputed s t r l e n ( p a t [ i l ) and stored it in the array at patlen[i]. If a graphics system needs to repeatedly compute a mathematical function like sine but only for a discrete set of values, such as integer degrees, it will be faster to precompute a table with 360 entries (or provide it as data) and index into it as needed. This is an example of trading space for time. There are many opportunities to replace code by data or to do computation during compilation, to save time and sometimes space as well. For example, the ctype functions like i s d i g i t are almost always implemented by indexing into a table of bit flags rather than by evaluating a sequence of tests. Use approximate values. If accuracy isn't an issue, use lower-precision data types. On older or smaller machines, or machines that simulate floating point in software, single-precision floating-point arithmetic is often faster than double-precision, so use f l o a t instead of double to save time. Some modern graphics processors use a related trick. The IEEE floating-point standard requires "graceful underflow" as calculations approach the low end of representable values, but this is expensive to compute. For images, the feature is unnecessary, and it is faster and perfectly acceptable to truncate to zero. This not only saves time when the numbers underflow, it can simplify the hardware for all arithmetic. The use of integer s i n and cos routines is another example of using approximate values. Rewrite in a lower-level language. Lower-level languages tend to be more efficient, although at a cost in programmer time. Thus rewriting some critical part of a C++ or Java program in C or replacing an interpreted script by a program in a compiled language may make it run much faster. Occasionally, one can get significant speedups with machine-dependent code. This is a last resort, not a step to be taken lightly, because it destroys portability and makes future maintenance and modifications much harder. Almost always, operations to be expressed in assembly language are relatively small functions that should be embedded in a library; memset and memmove, or graphics operations, are typical exam-
182
PERFORMANCE
CHAPTER 7
ples. The approach is to write the code as cleanly as possible in a high-level language and make sure it's correct by testing it as we described for memset in Chapter 6. This is your portable version, which will work everywhere, albeit slowly. When you move to a new environment, you can start with a version that is known to work. Now when you write an assembly-language version. test it exhaustively against the portable one. When bugs occur. non-portable code is always suspect: it's comforting to have a comparison implementation.
Exercise 7-4. One way to make a function like memset run faster is to have it write in word-sized chunks instead of byte-sized; this is likely to match the hardware better and might reduce the loop overhead by a factor of four or eight. The downside is that there are now a variety of end effects to deal with if the target is not aligned on a word boundary and if the length is not a multiple of the word size. Write a version of memset that does this optimization. Compare its performance to the existing library version and to a straightforward byte-at-a-time loop. Exercise 7-5. Write a memory allocator smalloc for C strings that uses a specialpurpose allocator for small strings but calls ma1 1oc directly for large ones. You will need to define a s t r u c t to represent the strings in either case. How do you decide where to switch from calling small oc to ma1 1oc?
7.5 Space Efficiency Memory used to be the most precious computing resource, always in short supply, and much bad programming was done in an attempt to squeeze the most out of what little there was. The infamous "Year 2000 Problem" is frequently cited as an example of this; when memory was truly scarce, even the two bytes needed to store 19 were deemed too expensive. Whether or not space is the true reason for the problem-such code may simply reflect the way people use dates in everyday life, where the century is commonly omitted-it demonstrates the danger inherent in short-sighted optimization. In any case, times have changed, and both main memory and secondary storage are amazingly cheap. Thus the first approach to optimizing space should be the same as to improving speed: don't bother. There are still situations, however, where space efficiency matters. If a program doesn't fit into the available main memory, parts of it will be paged out, and that will make its performance unacceptable. We see this when new versions of software squander memory; it is a sad reality that software upgrades are often followed by the purchase of more memory.
Save space by using the smallestpossible data type. One step to space efficiency is to make minor changes to use existing memory better. for example by using the smallest
SECTION 7.5
SPACE EFFICIENCY
183
data type that will work. This might mean replacing i n t with s h o r t if the data will fit; this is a common technique for coordinates in 2-D graphics systems, since 16 bits are likely to handle any expected range of screen coordinates. Or it might mean replacing doubl e with f 1o a t ; the potential problem is loss of precision, since f 1oats usually hold only 6 or 7 decimal digits. In these cases and analogous ones, other changes may be required as well, notably format specifications in p r i n t f and especially scanf statements. The logical extension of this approach is to encode information in a byte or even fewer bits, say a single bit where possible. Don't use C or C++ bitfields; they are highly non-portable and tend to generate voluminous and inefficient code. Instead, encapsulate the operations you want in functions that fetch and set individual bits within words or an array of words with shift and mask operations. This function returns a group of contiguous bits from the middle of a word: g e t b i t s : g e t n b i t s from p o s i t i o n p */ b i t s a r e numbered from 0 ( l e a s t s i g n i f i c a n t ) up */ unsigned i n t getbits(unsigned i n t x , i n t p, i n t n)
/a
/a
C return (x >> (p+l-n)) 81 -(-0
> operator may be arithmetic (a copy of the sign bit is propagated during the shift) or logical (zeros fill the vacated bits during the shift). Again, learning from the problems with C and C++, Java reserves >> for arithmetic right shift and provides a separate operator >>> for logical right shift. Byte order. The byte order within short, i n t , and l o n g is not defined; the byte with the lowest address may be the most significant byte or the least significant byte. This is a hardware-dependent issue that we'll discuss at length later in this chapter.
SECTION 8.1
LANGUAGE
195
Alignment of structure and class members. The alignment of items within structures, classes, and unions is not defined. except that members are laid out in the order of declaration. For example, in this structure, struct X { char c ; int i ;
I; the address of i could be 2,4, or 8 bytes from the beginning of the structure. A few machines allow i nts to be stored on odd boundaries, but most demand that an n-byte primitive data type be stored at an n-byte boundary, for example that doubles, which are usually 8 bytes long, are stored at addresses that are multiples of 8. On top of this, the compiler writer may make further adjustments, such as forcing alignment for performance reasons. You should never assume that the elements of a structure occupy contiguous memory. Alignment restrictions introduce "holes"; s t r u c t X will have at least one byte of unused space. These holes imply that a structure may be bigger than the sum of its member sizes, and will vary from machine to machine. If you're allocating memory to hold one, you must ask for si zeof ( s t r u c t X) bytes, not si zeof (char) + sizeof(int).
Bitfields. Bitfields are so machine-dependent that no one should use them. This long list of perils can be skirted by following a few rules. Don't use side effects except for a very few idiomatic constructions like
Don't compare a char to EOF. Always use s i z e o f to compute the size of types and objects. Never right shift a signed value. Make sure the data type is big enough for the range of values you are storing in it.
Try several compilers. It's easy to think that you understand portability, but compilers will see problems that you don't, and different compilers sometimes see your program differently, so you should take advantage of their help. Turn on all compiler warnings. Try multiple compilers on the same machine and on different machines. Try a C++ compiler on a C program. Since the language accepted by different compilers varies, the fact that your program compiles with one compiler is no guarantee that it is even syntactically correct. If several compilers accept your code, however, the odds improve. We have compiled every C program in this book with three C compilers on three unrelated operating systems (Unix, Plan 9, Windows) and also a couple of C++ compilers. This was a sobering experience, but it caught dozens of portability errors that no amount of human scrutiny would have uncovered. They were all trivial to fix.
196
PORTABILITY
CHAPTER 8
Of course, compilers cause portability problems too, by making different choices for unspecified behaviors. But our approach still gives us hope. Rather than writing code in a way that amplifies the differences among systems, environments, and compilers, we strive to create software that behaves independently of the variations. In short, we steer clear of features and properties that are likely to vary.
8.2 Headers and Libraries Headers and libraries provide services that augment the basic language. Examples include input and output through s t d i o in C, i ostream in C++, and java . i o in Java. Strictly speaking, these are not part of the language, but they are defined along with the language itself and are expected to be part of any environment that claims to support it. But because libraries cover a broad spectrum of activities, and must often deal with operating system issues, they can still harbor non-portabilities. Use standard libraries. The same general advice applies here as for the core language: stick to the standard, and within its older, well-established components. C defines a standard library of functions for input and output, string operations, character class tests, storage allocation, and a variety of other tasks. If you confine your operating system interactions to these functions, there is a good chance that your code will behave the same way and perform well as it moves from system to system. But you must still be careful, because there are many implementations of the library and some of them contain features that are not defined in the standard. ANSI C does not define the string-copying function strdup, yet most environments provide it, even those that claim to conform to the standard. A seasoned programmer may use strdup out of habit, and not be warned that it is non-standard. Later, the program will fail to compile when ported to an environment that does not provide the function. This sort of problem is the major portability headache introduced by libraries; the only solution is to stick to the standard and test your program in a wide variety of environments. Header files and package definitions declare the interface to standard functions. One problem is that headers tend to be cluttered because they are trying to cope with several languages in the same file. For example. it is common to find a single header file like stdio. h serving pre-ANSI C, ANSI C, and even C++ compilers. In such cases, the file is littered with conditional compilation directives like # i f and # i f def. Because the preprocessor language is not very flexible, the files are complicated and hard to read, and sometimes contain errors. This excerpt from a header file on one of our systems is better than most, because it is neatly formatted:
HEADERS AND LIBRARIES
SECTION 8.2
? ? ?
? ? ? ? ? ? ? ?
?
197
# i f d e f -OLD-C extern i n t f read() ; e x t e r n i n t fwrite() ; #else # i f d e f i ned(--STDC--) I I d e f ined(--cpl uspl us) e x t e r n s i ze-t f read(voi d* , s i z e - t, s i ze-t , FILE*) ; e x t e r n size- t fwrite(const v o i d * , s i z e - t , s i z e - t , FILE*) ; # e l s e /+ not --STDC-1 1 --cpluspl us */ e x t e r n s i ze-t f read() ; e x t e r n size- t f w r i t e o ; # e n d i f /a e l s e not --STDC-I I --cplusplus */ #endif
Even though the example is relatively clean, it demonstrates that header files (and programs) structured like this are intricate and hard to maintain. It might be easier to use a different header for each compiler or environment. This would require maintaining separate files, but each would be self-contained and appropriate for a particular system, and would reduce the likelihood of errors like including strdup in a strict ANSI C environment. Header files also can "pollute" the name space by declaring a function with the same name as one in your program. For example, our warning-message function wepri n t f was originally called w p r i n t f , but we discovered that some environments, in anticipation of the new C standard, define a function with that name in s t d i o . h. We needed to change the name of our function in order to compile on those systems and be ready for the future. If the problem was an erroneous implementation rather than a legitimate change of specification, we could work around it by redefining the name when including the header: ? ? ?
? ?
/* some versions of s t d i o use w p r i n t f so d e f i n e i t away: a/ #define w p r i n t f stdio- wprintf # i ncl ude <stdio h> #undef w p r i n t f /* code using our w p r i n t f 0 follows.. . */
.
This maps all occurrences of w p r i n t f in the header file to stdio- wprintf so they will not interfere with our version. We can then use our own wpri n t f without changing its name, at the cost of some clumsiness and the risk that a library we link with will call our wpri n t f expecting to get the official one. For a single function, it's probably not worth the trouble, but some systems make such a mess of the environment that one must resort to extremes to keep the code clean. Be sure to comment what the construction is doing, and don't make it worse by adding conditional compilation. If some environments define wpri n t f , assume they all do; then the fix is permanent and you won't have to maintain the # i f d e f statements as well. It may be easier to switch than fight and it's certainly safer, so that's what we did when we changed the name to weprintf. Even if you try to stick to the rules and the environment is clean. it is easy to step outside the limits by implicitly assuming that some favorite property is true every-
198
PORTABILITY
CHAPTER 8
where. For instance, ANSI C defines six signals that can be caught with signal; the POSlX standard defines 19; most Unix systems support 32 or more. If you want to use a non-ANSI signal, there is clearly a tradeoff between functionality and portability. and you must decide which matters more. There are many other standards that are not part of a programming language definition; examples include operating system and network interfaces, graphics interfaces, and the like. Some are meant to carry across more than one system, like POSIX; others are specific to one system, like the various Microsoft Windows APls. Similar advice holds here as well. Your programs will be more portable if you choose widely used and well-established standards, and if you stick to the most central and commonly used aspects.
8.3 Program Organization There are two major approaches to portability, which we will call union and intersection. The union approach is to use the best features of each particular system, and make the compilation and installation process conditional on properties of the local environment. The resulting code handles the union of all scenarios, taking advantage of the strengths of each system. The drawbacks include the size and complexity of the installation process and the complexity of code riddled with compile-time conditionals.
Use only features available everywhere. The approach we recommend is intersection: use only those features that exist in all target systems; don't use a feature if it isn't available everywhere. One danger is that the requirement of universal availability of features may limit the range of target systems or the capabilities of the program; another is that performance may suffer in some environments. To compare these approaches, let's look at a couple of examples that use union code and rethink them using intersection. As you will see, union code is by design unportable. despite its stated goal, while intersection code is not only portable but usually simpler. This small example attempts to cope with an environment that for some reason doesn't have the standard header file s t d l i b. h: ? ? ? ? ?
?
# i f defined (STDC-HEADERS) 1 I defined ( L I B C ) #include<stdlib.h> #else extern void *malloc(unsigned i n t ) ; extern void *realloc(void *, unsigned i n t ) ; #endif
This style of defense is acceptable if used occasionally, but not if it appears often. It also begs the question of how many other functions from s t d l i b will eventually find their way into this or similar conditional code. If one is using ma1 1oc and real 1oc,
PROGRAM ORGANIZATION
SECTION 8.3
199
surely f r e e will be needed as well, for instance. What if unsigned i n t is not the same as s i ze-t, the proper type of the argument to ma1 1oc and real 1oc? Moreover, how do we know that STDC-HEADERS or -LIBC are defined, and defined correctly? How can we be sure that there is no other name that should trigger the substitution in some environment? Any conditional code like this is incomplete-unportablebecause eventually a system that doesn't match the condition will come along, and we must edit the #ifdefs. If we could solve the problem without conditional compilation, we would eliminate the ongoing maintenance headache. Still, the problem this example is solving is real. so how can we solve it once and for all? Our preference would be to assume that the standard headers exist; it's someone else's problem if they don't. Failing that, it would be simpler to ship with the software a header file that defines ma1 loc, real loc, and free, exactly as ANSI C defines them. This file can always be included, instead of applying band-aids throughout the code. Then we will always know that the necessary interface is available.
Avoid conditional compilation. Conditional compilation with #ifdef and similar preprocessor directives is hard to manage, because information tends to get sprinkled throughout the source. # i f def NATIVE char rastring = "convert #el se #i fdef MAC char *astring = "convert #el se #ifdef DOS char *astring = "convert #el se char aastring = "convert #endif /* ?DOS r / #endif /* ?MAC a/ #endif /* ?NATIVE */
ASCII t o native character s e t " ; t o Mac t e x t f i l e format"; t o DOS t e x t f i l e format"; t o Unix t e x t f i l e format";
This excerpt would have been better with #el i f after each definition. rather than having #endi fs pile up at the end. But the real problem is that, despite its intention, this code is highly non-portable because it behaves differently on each system and needs to be updated with a new #ifdef for every new environment. A single string with more general wording would be simpler. completely portable, and just as informative: char rastring
=
"convert t o local t e x t format";
This needs no conditional code since it is the same on all systems. Mixing compile-time control flow (determined by #i fdef statements) with runtime control flow is much worse, since it is very difficult to read.
200
CHAPTER 8
PORTABILITY
# i f ndef DISKSYS f o r (i = 1; i dbgmsg.msg-total; i++) #endi f #i fdef DISKSYS i = dbgmsgno; i f (i dbgmsg. msg-total) #endi f
C
...
i f (msg->dbgmsg.msg-total == i ) #i f ndef DISKSYS break; /* no more messages t o wait f o r */ about 30 more lines, with further conditional compilation #endi f
3 Even when apparently innocuous, conditional compilation can frequently be replaced by cleaner methods. For instance, #ifdefs are often used to control debugging code: ?
? ?
#ifdef DEBUG p r i n t f (.. .) ; #endif
but a regular i f statement with a constant condition may work just as well: enum { DEBUG
...
=
0 3;
i f (DEBUG) { printf (.. .);
3 If DEBUG is zero, most compilers won't generate any code for this, but they will check the syntax of the excluded code. An #ifdef, by contrast, can conceal syntax errors that will prevent compilation if the #i fdef is later enabled. Sometimes conditional compilation excludes large blocks of code: #ifdef notdef
/* undefined symbol */
but conditional code can often be avoided altogether by using files that are conditionally substituted during compilation. We will return to this topic in the next section. When you must modify a program to adapt to a new environment, don't begin by making a copy of the entire program. Instead, adapt the existing source. You will
SECTION 8.3
PROGRAM ORGANIZATION
201
probably need to make changes to the main body of the code, and if you edit a copy, before long you will have divergent versions. As much as possible. there should only be a single source for a program; if you find you need to change something to port to a particular environment, find a way to make the change work everywhere. Change internal interfaces if you need to, but keep the code consistent and #ifdef-free. This will make your code more portable over time, rather than more specialized. Narrow the intersection, don't broaden the union. We have spoken out against conditional compilation and shown some of the problems it causes. But the nastiest problem is one we haven't mentioned: it is almost impossible to test. An #ifdef turns a single program into two separately-compiled programs. It is difficult to know whether all the variant programs have been compiled and tested. If a change is made in one #ifdef block, we may need to make it in others, but the changes can be verified only within the environment that causes those #i fdefs to be enabled. If a similar change needs to be made for other configurations, it cannot be tested. Also, when we add a new #ifdef block, it is hard to isolate the change to determine what other conditions need to be satisfied to get here, and where else this problem might need to be fixed. Finally, if something is in code that is conditionally omitted, the compiler doesn't see it. It could be utter nonsense and we won't know until some unlucky customer tries to compile it in the environment that triggers that condition. This program compiles when -MAC is defined and fails when it is not: #ifdef -MAC pri ntf ("Thi s i s Mad ntosh\rU) ; #el se This will give a syntax error on other systems #endi f So our preference is to use only features that are common to all target environments. We can compile and test all the code. If something is a portability problem, we rewrite to avoid it rather than adding conditional code; this way, portability will steadily increase and the program itself will improve rather than becoming more complicated. Some large systems are distributed with a configuration script to tailor code to the local envimnment. At compilation time, the script tests the envimnment properties-location of header files and libraries, byte order within words, size of types, implementations known to be broken (surprisingly common), and so on-and generates configuration parameters or makefiles that will give the right configuration settings for that situation, These scripts can be large and intricate, a significant fraction of a software distribution, and require continual maintenance to keep them working. Sometimes such techniques are necessary but the more portable and #i fdef-free the code is, the simpler and more reliable the configuration and installation will be.
Exercise 8-1. Investigate how your compiler handles code contained within a conditional block like
202
CHAPTER 8
PORTABILITY
const i n t DEBUG
=
0;
/* or enum { DEBUG = 0 3 ; /* or f i n a l boolean DEBUG i f (DEBUG)
a/ =
fa1s e ; */
{
Under what circumstances does it check syntax? When does it generate code? If you have access to more than one compiler, how do the results compare?
8.4 Isolation Although we would like to have a single source that compiles without change on all systems, that may be unrealistic. But it is a mistake to have non-portable code scattered throughout a program: that is one of the problems that conditional compilation creates.
Localize system dependencies in separate files. When different code is needed for different systems, the differences should be localized in separate files, one file for each system. For example, the text editor Sam runs on Unix, Windows, and several other operating systems. The system interfaces for these environments vary widely, but most of the code for Sam is identical everywhere. A single file captures the system variations for a particular environment; uni x. c provides the interface code for Unix systems, and windows. c for the Windows environment. These files implement a portable interface to the operating system and hide the differences. Sam is, in effect, written to its own virtual operating system, which is ported to various real systems by writing a couple of hundred lines of C to implement half a dozen small but nonportable operations using locally available system calls. The graphics environments of these operating systems are almost unrelated. Sam copes by having a portable library for its graphics. Although it's a lot more work to build such a library than to hack the code to adapt to a given system-the code to interface to the X Window system, for example, is about half as big as the rest of Sam put together-the cumulative effort is less in the long run. And as a side benefit, the graphics library is itself valuable, and has been used separately to make a number of other programs portable, too. Sam is an old program; today, portable graphics environments such as OpenGL. Tcmk and Java are available for a variety of platforms. Writing your code with these rather than a proprietary graphics library will give your program wider utility. Hide system dependencies behind interfaces. Abstraction is a powerful technique for creating boundaries between portable and non-portable parts of a program. The 110 libraries that accompany most programming languages provide a good example: they present an abstraction of secondary storage in terms of files to be opened and closed,
SECTION 8.5
DATA EXCHANGE
203
read and written, without any reference to their physical location or structure. Programs that adhere to the interface will run on any system that implements it. The implementation of Sam provides another example of abstraction. An interface is defined for the file system and graphics operations and the program uses only features of the interface. The interface itself uses whatever facilities are available in the underlying system. That might require significantly different implementations on different systems, but the program that uses the interface is independent of that and should require no changes as it is moved. The Java approach to portability is a good example of how far this can be carried. A Java program is translated into operations in a "virtual machine." that is, a simulated computer that can be implemented to run on any real machine. Java libraries provide uniform access to features of the underlying system, including graphics, user interface, networking, and the like; the libraries map into whatever the local system provides. In theory, it should be possible to run the same Java program (even after translation) everywhere without change.
8.5 Data Exchange Textual data moves readily from one system to another and is the simplest portable way to exchange arbitrary information between systems.
Use text for data exchange. Text is easy to manipulate with other tools and to process in unexpected ways. For example, if the output of one program isn't quite right as input for another, an Awk or Per1 script can be used to adjust it; grep can be used to select or discard lines; your favorite editor can be used to make more complicated changes. Text files are also much easier to document and may not even need much documentation, since people can read them. A comment in a text file can indicate what version of software is needed to process the data; the first line of a Postscript file, for instance, identifies the encoding:
By contrast, binary files need specialized tools and rarely can be used together even on the same machine. A variety of widely-used programs convert arbitrary binary data into text so it can be shipped with less chance of corruption; these include b i nhex for Macintosh systems, uuencode and uudecode for Unix, and various tools that use MIME encoding for transferring binary data in mail messages. In Chapter 9, we show a family of pack and unpack routines to encode binary data portably for transmission. The sheer variety of such tools speaks to the problems of binary formats. There is one continuing irritation with exchanging text: PC systems use a carriage return ' \ r ' and a newline or line-feed '\n' to terminate each line, while Unix systems use only newline. The carriage return is an artifact of an ancient device called a
204
PORTABILITY
CHAPTER 8
Teletype that had a carriage-return (CR) operation to return the typing mechanism to the beginning of a line, and a separate line-feed operation (LF) to advance it to the next line. Even though today's computers have no carriages to return, PC software for the most part continues to expect the combination (familiarly known as CRLF, pronounced "curliff ') on each line. If there are no carriage returns, a file may be interpreted as one giant line. Line and character counts can be wrong or change unexpectedly. Some software adapts gracefully, but much does not. PCs are not the only culprits; thanks to a sequence of incremental compatibilities, some modem networking standards such as HTTP also use CRLF to delimit lines. Our advice is to use standard interfaces, which will treat CRLF consistently on any given system, either (on PCs) by removing \ r on input and adding it back on output, or (on Unix) by always using \n rather than CRLF to delimit lines in files. For files that must be moved back and forth, a program to convert files from each format to the other is a necessity.
Exercise 8-2. Write a program to remove spurious carriage returns from a file. Write a second program to add them by replacing each newline with a carriage return and newline. How would you test these programs?
8.6 Byte Order Despite the disadvantages discussed above, binary data is sometimes necessary. It can be significantly more compact and faster to decode, factors that make it essential for many problems in computer networking. But binary data has severe portability problems. At least one issue is decided: all modem machines have 8-bit bytes. Different machines have different representations of any object larger than a byte, however, so relying on specific properties is a mistake. A short integer (typically 16 bits, or two bytes) may have its low-order byte stored at a lower address than the high-order byte (little-endian). or at a higher address (big-endian). The choice is arbitrary, and some machines even support both modes. Therefore, although big- and little-endian machines see memory as a sequence of words in the same order, they interpret the bytes within a word in the opposite order. In this diagram, the four bytes starting at location 0 will represent the hexadecimal integer 0x11223344 on a big-endian machine and 0x44332211 on a little-endian. 0
1
2
3
4
5
6
7
8
To see byte order in action, try this program:
SECTION 8.6
/* byteorder: d i s p l a y b y t e s o f a long
u/
i n t mai n (voi d)
C
unsigned long x ; unsigned c h a r *p; int i ; /* 11 22 33 44 => big-endian u / /* 44 33 22 11 => l i t t l e - e n d i a n */ / u x = Ox1122334455667788UL; f o r 6 4 - b i t long x = Ox11223344UL; p = (unsigned char *) &x; f o r (i = 0 ; i < sizeof(1ong); i++) p r i n t f ("%x " , *p++) ; p r i n t f ("\nu); return 0;
u/
I On a 32-bit big-endian machine, the output is
but on a little-endian machine. it is
and on the PDP-11 (a vintage 16-bit machine still found in embedded systems), it is
On machines with 64-bit longs. we can make the constant bigger and see similar behaviors. This may seem like a silly program, but if we wish to send an integer down a byte-wide interface such as a network connection, we need to choose which byte to send first, and that choice is in essence the big-endiannittle-endian decision. In other words, this program is doing explicitly what fwrite(&x, sizeof(x), 1 , stdout); does implicitly. It is not safe to write an i n t (or s h o r t or long) from one computer and read it as an i n t on another computer. For example, if the source computer writes with unsigned s h o r t x ; fwrite(&x, s i z e o f (x). 1, s t d o u t ) ; and the receiving computer reads with unsigned s h o r t x; fread(&x, s i z e o f (x) , 1 , s t d i n ) ; the value of x will not be preserved if the machines have different byte orders. If x starts as 0x1000 it may arrive as 0x0010.
206
PORTABILITY
CHAPTER 8
This problem is frequently solved using conditional compilation and "byte swapping," something like this: ? ? ? ? ? ?
short x; fread(&x,sizeof(x),l,stdin); # i f d e f BIG-ENDIAN / a swap b y t e s a/ x = ((x&OxFF) >8) & OXFF); #endif
This approach becomes unwieldy when many two- and four-byte integers are being exchanged. In practice, the bytes end up being swapped more than once as they pass from place to place. If the situation is bad for short, it's worse for longer data types, because there are more ways to permute the bytes. Add in the variable padding between structure members, alignment restrictions, and the mysterious byte orders of older machines, and the problem looks intractable. Use a fmed byte order for data exchange. There is a solution. Write the bytes in a canonical order using portable code: unsigned s h o r t x ; putchar(x >> 8) ; putcharcx & OxFF);
/a /a
w r i t e high- order b y t e a/ w r i t e low- order b y t e a/
then read it back a byte at a time and reassemble it: unsigned s h o r t x ; x = getchar() = 0 && $ l i n e != "") I) ;# s k i p header puts [read Bsol ;# read and p r i n t e n t i r e r e p l y
This script typically produces voluminous output, much of which is HTML tags bracketed by < and >. Perl is good at text substitution, so our next tool is a Perl script that uses regular expressions and substitutions to discard the tags:
.
# unhtml p l : delete HTML tags
while (o) { $ s t r .= $-;
# c o l l e c t a l l input i n t o single s t r i n g # by concatenating i n p u t 1ines
1 Bstr =- s///g; Bstr =- s/Â / /g; Bstr =- s/\s+/\n/g; p r i n t $str;
.
# delete # replace  by blank # compress white space
SECTION 9.4
INTERPRETERS. COMPILERS, AND VIRTUAL MACHINES
231
This example is cryptic if one does not speak Perl. The construction
substitutes the string rep1 for the text in s t r that matches (leftmost longest) the regular expression regexp; the trailing g, for "global," means to do so for all matches in the string rather than just the first. The metacharacter sequence \s is shorthand for a white space character (blank, tab, newline, and the like); \n is a newline. The string "  ;" is an HTML character, like those in Chapter 2, that defines a non-breakable space character. Putting all this together, here is a moronic but functional web browser, implemented as a one-line shell script: # web: r e t r i e v e web page and format i t s t e x t , ignoring HTML
g e t u r l . t c l $1 I unhtml . p l
I f m t .awk
This retrieves the web page, discards all the control and formatting information, and formats the text by its own rules. It's a fast way to grab a page of text from the web. Notice the variety of languages we cascade together, each suited to a particular task: Tcl, Perl, Awk and, within each of those, regular expressions. The power of notation comes from having a good one for each problem. Tcl is particularly good for grabbing text over the network; Perl and Awk are good at editing and formatting text; and of course regular expressions are good at specifying pieces of text for searching and modifying. These languages together are more powerful than any one of them in isolation. It's worth breaking the job into pieces if it enables you to profit from the right notation.
9.4 Interpreters, Compilers, and Virtual Machines How does a program get from its source-code form into execution? If the language is simple enough, as in p r i n t f or our simplest regular expressions, we can execute straight from the source. This is easy and has very fast startup. There is a tradeoff between setup time and execution speed. If the language is more complicated, it is generally desirable to convert the source code into a convenient and efficient internal representation for execution. It takes some time to process the source originally but this is repaid in faster execution. Programs that combine the conversion and execution into a single program that reads the source text, converts it. and runs it are called interpreters. Awk and Perl interpret, as do many other scripting and special-purpose languages. A third possibility is to generate instructions for the specific kind of computer the program is meant to run on, as compilers do. This requires the most up-front effort and time but yields the fastest subsequent execution.
232
NOTATION
CHAPTER 9
Other combinations exist. One that we will study in this section is compiling a program into instructions for a made-up computer (a virtual machine) that can be simulated on any real computer. A virtual machine combines many of the advantages of conventional interpretation and compilation. If a language is simple, it doesn't take much processing to infer the program stmcture and convert it to an internal form. If, however, the language has some complexity-declarations. nested structures, recursively-defined statements or expressions, operators with precedence, and the like-it is more complicated to parse the input to determine the structure. Parsers are often written with the aid of an automatic parser generator, also called a compiler-compiler. such as yacc or bison. Such programs translate a description of the language, called its grammar, into (typically) a C or C++ program that, once compiled, will translate statements in the language into an internal representation. Of course, generating a parser directly from a grammar is another demonstration of the power of good notation. The representation produced by a parser is usually a tree, with internal nodes containing operators and leaves containing operands. A statement such as
might produce this parse (or syntax) tree:
Many of the tree algorithms described in Chapter 2 can be used to build and process parse trees. Once the tree is built, there are a variety of ways to proceed. The most direct, used in Awk, is to walk the tree directly, evaluating the nodes as we go. A simplified version of such an evaluation routine for an integer-based expression language might involve a post-order traversal like this: typedef s t r u c t Symbol Symbol ; typedef s t r u c t Tree Tree; s t r u c t Symbol { in t v a l ue ; char *name;
I;
SECTION 9.4
INTERPRETERS, COMPILERS. AND VIRTUAL MACHINES
s t r u c t Tree in t in t Symbol Tree Tree
233
{
OP; v a l ue ; *symbol ; *left; aright;
operation code */ /* value i f number */ /a
/a
Symbol e n t r y i f v a r i a b l e
a/
3; /a eval: version 1: evaluate t r e e expression */ in t eval (Tree *t)
i n t l e f t , right; switch (t->op) { case NUMBER: r e t u r n t->val ue; case VARIABLE: r e t u r n t->symbol ->val ue ; case ADD: r e t u r n eval ( t - > l e f t ) + eval ( t - > r i g h t ) ; case DIVIDE: 1e f t = eval ( t - > l e f t ) ; r i g h t = eval (t- >right) ; i f ( r i g h t == 0) e p r i n t f ( " d i v i de %d by zero", 1e f t ) ; return l e f t / right; case MAX: 1e f t = eval ( t - > l e f t ) ; r i g h t = eval ( t - > r i g h t ) ; return l e f b r i g h t ? l e f t : right; case ASSIGN: t->left- >symbol ->value = eval ( t - > r i g h t ) ; r e t u r n t->left->symbol- >value;
/* 1
...
*/
3
The first few cases evaluate simple expressions like constants and values; later ones evaluate arithmetic expressions, and others might do special processing, conditionals, and loops. To implement control structures, the tree will need extra information, not shown here, that represents the control flow. As in pack and unpack, we can replace the explicit switch with a table of function pointers. Individual operators are much the same as in the switch statement: /a addop: r e t u r n sum o f two t r e e expressions */ i n t addop(Tree a t )
{
r e t u r n eval ( t - > l e f t )
+
eval ( t - > r i g h t ) ;
3 The table of function pointers relates operators to the functions that perform the operations:
CHAPTER 9
enum { /* operation codes, Tree.op NUMBER, VARIABLE, ADD, DIVIDE, /a
...
*/
a/
1;
/* optab: operator f u n c t i o n t a b l e i n t (*optabCl) (Tree a) = { pushop, /* NUMBER */ pushsymop, /* VARIABLE a/ addop /* ADD +/ /ir D I V I D E n/ d i vop
*/
.. /* ... */
I; Evaluation uses the operator to index into the table of function pointers to call the right functions; this version will invoke other functions recursively.
/* eval : version 2 : evaluate t r e e from operator tab1 e */ in t eval (Tree *t) r e t u r n (*optab[t->op])
(t) ;
1 Both these versions of eval are recursive. There are ways of eliminating recursion, including a clever technique called threaded code that flattens the call stack completely. The neatest method is to do away with the recursion altogether by storing the functions in an array that is then traversed sequentially to execute the program. This array becomes a sequence of instructions to be executed by a little specialpurpose machine. We still need a stack to represent the partially evaluated values in the computation, so the form of the functions changes, but the transformation is easy to see. In effect, we invent a stack machine in which the instructions are tiny functions and the operands are stored on a separate operand stack. It's not a real machine but we can program it as if it were, and we can implement it easily as an interpreter. Instead of walking the tree to evaluate it, we walk it to generate the array of functions to execute the program. The array will also contain data values that the instructions use, such as constants and variables (symbols), so the type of the elements of the array should be a union: typedef union Code Code; union Code { void (*op)(void); in t v a l ue ; Symbol *symbol ;
I;
/a /a /a
f u n c t i o n i f operator a/ value i f number */ Symbol e n t r y i f v a r i a b l e
a/
INTERPRETERS, COMPILERS. AND VIRTUAL MACHINES
SECTION 9.4
235
Here is the routine to generate the function pointers and place them in an array, code. of these items. The return value of generate is not the value of the expression-that will be computed when the generated code is executed-but the index in code of the next operation to be generated: /n generate: generate i n s t r u c t i o n s by walking t r e e i n t generate(int codep, Tree a t )
*/
C switch (t->op) { case NUMBER: code [codep++l . op = pushop; code [codep++] v a l ue = t->val ue; r e t u r n codep; case VARIABLE: code[codep++].op = pushsymop; code [codep++l symbol = t->symbol ; r e t u r n codep; case ADD: codep = generateccodep, t - > l e f t ) ; codep = generateccodep, t - > r i g h t ) ; code [codep++l op = addop ; r e t u r n codep; case DIVIDE: codep = generate(codep, t - > l e f t ) ; codep = generate(codep, t - > r i g h t ) ; code [codep++] op = d i vop ; r e t u r n codep; case MAX:
.
.
.
.
/*
...
*/
1 1 For the statement a = max(b , c/2) the generated code would look like this: pushsymop b pushsymop C
pus hop 2 d i vop maxop storesymop a
The operator functions manipulate the stack, popping operands and pushing results. The interpreter is a loop that walks a program counter along the array of function pointers:
CHAPTER 9
Code code[NCODEl ; in t stackCNSTACK1; i n t stackp; i n t pc; /* program counter a /
/* eval : version 3: evaluate expression from generated code */ in t eval (Tree
at)
C pc = generate(0, t) ; code [pcl . op = NULL; stackp = 0; PC = 0; whi 1e (code [pc] . op ! = NULL) (acode [PC++] . op) 0; r e t u r n stack[Ol;
I This loop simulates in software on our invented stack machine what happens in hardware on a real machine. Here are a couple of representative operators: /a pushop: push number; value i s next word i n code stream voi d pushop(voi d)
*/
C stack[stackp++l = code [PC++] .value;
1 /* divop: compute r a t i o o f two expressions voi d d i vop (voi d)
*/
C i n t l e f t , right; r i g h t = stack[--stackpl; l e f t = stack[--stackp]; i f ( r i g h t == 0) e p r i n t f (" divide %d by zero\nW, l e f t ) ; stack[stackp++] = l e f t / r i g h t ;
1 Notice that the check for zero divisors appears in divop, not generate. Conditional execution, branches, and loops operate by modifying the program counter within an operator function, performing a branch to a different point in the array of functions. For example a goto operator always sets the value of the pc variable, while a conditional branch sets pc only if the condition is true. The code array is internal to the interpreter, of course, but imagine we wanted to save the generated program in a file. If we wrote out the function addresses, the result would be unponable and fragile. But we could instead write out constants that represented the functions, say 1000 for addop. 1001 for pushop, and so on, and translate these back into the function pointers when we read the program in for interpretation. If we examine a file this procedure produces, it looks like an instruction stream for a virtual machine whose instructions implement the basic operators of our little lan-
SECTION 9.5
PROGRAMS THAT WRITE PROGRAMS
237
guage, and the generate function is really a compiler that translates the language into the virtual machine. Virtual machines are a lovely old idea. recently made fashionable again by Java and the Java Virtual Machine (JVM); they give an easy way to produce portable, efficient representations of programs written in a high-level language.
9.5 Programs that Write Programs Perhaps the most remarkable thing about the generate function is that it is a program that writes a program: its output is an executable instruction stream for another (virtual) machine. Compilers do this all the time, translating source code into machine instructions, so the idea is certainly familiar. In fact, programs that write programs appear in many forms. One common example is the dynamic generation of HTML for web pages. HTML is a language, however limited, and it can contain JavaScript code as well. Web pages are often generated on the fly by Per1 or C programs, with specific contents (for example, search results and targeted advertising) determined by incoming requests. We used specialized languages for the graphs, pictures, tables, mathematical expressions, and index in this book. As another example, PostScript is a programming language that is generated by word processors. drawing programs, and a variety of other sources; at the final stage of processing, this whole book is represented as a 57,000 line Postscript program. A document is a static program, but the idea of using a programming language as notation for any problem domain is extremely powerful. Many years ago, programmers dreamt of having computers write all their programs for them. That will probably never be more than a dream, but today computers routinely write programs for us. often to represent things we would not previously have considered programs at all. The most common program-writing program is a compiler that translates highlevel language into machine code. It's often useful, though, to translate code into a mainstream programming language. In the previous section, we mentioned that parser generators convert a definition of a language's grammar into a C program that parses the language. C is often used in this way, as a kind of "high level assembly language." Modula-3 and C++ are among the general-purpose languages whose first compilers created C code, which was then compiled by a standard C compiler. The approach has several advantages, including efficiency-because programs can in principle run as fast as C programs-and portability-because compilers can be carried to any system that has a C compiler. This greatly helped the early spread of these languages. As another example, Visual Basic's graphical interface generates a set of Visual Basic assignment statements to initialize objects that the user has selected from menus and positioned on the screen with a mouse. A variety of other languages have "visual" development systems and "wizards" that synthesize user-interface code out of mouse clicks.
238
NOTATION
CHAPTER 9
In spite of the power of program generators, and in spite of the existence of many good examples, the notion is not appreciated as much as it should be and is infrequently used by individual programmers. But there are plenty of small-scale opportunities for creating code by a program, so that you can get some of the advantages for yourself. Here are several examples that generate C or C++ code. The Plan 9 operating system generates error messages from a header file that contains names and comments; the comments are converted mechanically into quoted strings in an array that can be indexed by the enumerated value. This fragment shows the structure of the header file:
/*
e r r o r s -h : standard e r r o r messages a/
enum { Epe r m, Eio, Efile, Emem, Espace Eg reg
I;
/* Permission denied */
/* /*
.
/* /* /*
1/0 e r r o r a/ F i l e does not e x i s t */ Memory l i m i t reached */ Out o f f i l e space */ I t ' s a l l Greg's f a u l t */
Given this input. a simple program can produce the following set of declarations for the error messages:
/*
machine-generated; do n o t e d i t .
*/
char * e r r s [ ] = { "Permission denied", /* Eperm */ "I/O e r r o r " , /* E i o */ " F i l e does n o t e x i s t " , /* E f i l e */ "Memory l i m i t reached", /* Emem */ "Out o f f i l e space", /* Espace */ " I t ' s a l l Greg's f a u l t " . /* Egreg */
I; There are a couple of benefits to this approach. First, the relationship between the enum values and the strings they represent is literally self-documenting and easy to
make natural-language independent. Also, the information appears only once, a "single point of truth" from which other code is generated, so there is only one place to keep information up to date. If instead there are multiple places, it is inevitable that they will get out of sync sometime. Finally, it's easy to arrange that the . c file will be recreated and recompiled whenever the header file is changed. When an error message must be changed, all that is needed is to modify the header file and compile the operating system. The messages are automatically updated. The generator program can be written in any language. A string processing language like Per1 makes it easy:
SECTION 9.5
PROGRAMS THAT WRITE PROGRAMS
239
# enum.pl: generate e r r o r s t r i n g s from enum+comments
p r i n t "/* machine-generated; do not e d i t . */\n\nW; p r i n t "char * e r r s [ ] = {\nW; w h i l e (o) { chop; i f (/A\s*(E[a-z0-9]+), $name = $1; S/ *\/\* *// ;
.
S/
*\*\///;
p r i n t "\t\"$-\",
I
remove newline f i r s t word i s E . save name remove up t o /* remove */ $name */\nW;
?/)
/*
{
# # # # #
..
I
p r i n t "};\nn;
Regular expressions are in action again. Lines whose first fields look like identifiers followed by a comma are selected. The first substitution deletes everything up to the first non-blank character of the comment, while the second removes the comment terminator and any blanks that precede it. As part of a compiler-testing effort, Andy Koenig developed a convenient way to write C++ code to check that the compiler caught program errors. Code fragments that should cause a compiler diagnostic are decorated with magic comments to describe the expected messages. Each line has a comment that begins with /// (to distinguish it from ordinary comments) and a regular expression that matches the diagnostics from that line. Thus, for example, the following two code fragments should generate diagnostics: int
f0 {I /// warning. * non-void f u n c t i o n
void g()
.*
should r e t u r n a value
{ r e t u r n 1;)
/// e r r o r . * void f u n c t i o n may n o t r e t u r n a value If we run the second test through our C++ compiler, it prints the expected message, which matches the regular expression: % CC x . c " x - c " , l i n e 1: error(321):
void f u n c t i o n may n o t r e t u r n a value
Each such code fragment is given to the compiler, and the output is compared against the expected diagnostics, a process that is managed by a combination of shell and Awk programs. Failures indicate a test where the compiler output differed from what was expected. Because the comments are regular expressions there is some latitude in the output; they can be made more or less forgiving, depending on what is needed. The idea of comments with semantics is not new. They appear in Postscript, where regular comments begin with %. Comments that begin with %% by convention may carry extra information about page numbers, bounding boxes, font names, and the like:
CHAPTER 9
%%PageBoundingBox:126 307 492 768 %%Pages: 14 %%DocumentFonts: Helveti ca Times-Ital i c Times-Roman Luci daSans-Typewri t e r In Java, comments that begin with /** and end with */ are used to create documentation for the class definition that follows. The large-scale version of self-documenting code is literate programming, which integrates a program and its documentation so one process prints it in a natural order for reading, and another arranges it in the right order for compilation. In all of the examples above, it is important to observe the role of notation, the mixture of languages, and the use of tools. The combination magnifies the power of the individual components.
Exercise 9-15. One of the old chestnuts of computing is to write a program that when executed will reproduce itself exactly, in source form. This is a neat special case of a program that writes a program. Give it a try in some of your favorite languages.
9.6 Using Macros to Generate Code Descending a couple of levels, it's possible to have macros write code at compile time. Throughout this book, we've cautioned against using macros and conditional compilation; they encourage a style of programming that is full of problems. But they do have their place; sometimes textual substitution is exactly the right answer to a problem. One example is using the C/C++ macro preprocessor to assemble pieces of a stylized, repetitive program. For instance, the program that estimated the speed of elementary language constructs for Chapter 7 uses the C preprocessor to assemble the tests by wrapping them in boilerplate code. The essence of the test is to encapsulate a code fragment in a loop that starts a timer, runs the fragment many times, stops the timer, and reports the results. All of the repeated code is captured in a couple of macros, and the code to be timed is passed in as an argument. The primary macro takes this form: #define LOOP(C0DE) { t o = clock(); f o r (i = 0 ; i < n; i++) { CODE; I p r i n t f ("%7d ", clock() - t o ) ;
\ \ \ \
I The backslashes allow the macro body to span multiple lines. This macro is used in "statements" that typically look like this:
SECTION 9.7
COMPILING ON THE FLY
241
There are sometimes other statements for initialization, but the basic timing part is represented in these single-argument fragments that expand to a significant amount of code. Macro processing can be used to generate production code, too. Bart Locanthi once wrote an efficient version of a two-dimensional graphics operator. The operator, called b i t b l t or rasterop, is hard to make fast because there are many arguments that combine in complicated ways. Through careful case analysis, Locanthi reduced the combinations to individual loops that could be separately optimized. Each case was then constructed by macro substitution, analogous to the performance-testing example, with all the variants laid out in a single big switch statement. The original source code was a few hundred lines; the result of macro processing was several thousand. The macro-expanded code was not optimal but, considering the difficulty of the problem, it was practical and very easy to produce. Also. as high-performance code goes, it was relatively portable.
Exercise 9-16. Exercise 7-7 involved writing a program to measure the cost of various operations in C++. Use the ideas of this section to create another version of the program.
Exercise 9-17. Exercise 7-8 involved doing a cost model for Java, which has no macro capability. Solve the problem by writing another program, in whatever language (or languages) you choose, that writes the Java version and automates the timing runs.
9.7 Compiling on the Fly In the previous section, we talked about programs that write programs. In each of the examples, the generated program was in source form; it still needed to be compiled or interpreted to run. But it is possible to generate code that is ready to run immediately by producing machine instructions rather than source. This is known as compiling "on the fly" or "just in time"; the first term is older but the latter. including its acronym, JIT, is more popular. Although compiled code is necessarily non-portable-it will run only on a single type of processor-it can be extremely fast. Consider the expression
The calculation must evaluate c, divide it by two, compare the result to b, and choose the larger. If we evaluate the expression using the virtual machine we sketched earlier in the chapter, we could eliminate the check for division by zero in divop. Since 2 is never zero, the check is pointless. But given any of the designs we laid out for implementing the virtual machine, there is no way to eliminate the check; every implementation of the divide operation compares the divisor to zero.
242
NOTATION
CHAPTER 9
This is where generating code dynamically can help. If we build the code for the expression directly, rather than just by stringing out predefined operations, we can avoid the zero-divide check for divisors that are known to be non-zero. In fact, we can go even further; if the entire expression is constant. such as max(3n3, 4/2), we can evaluate it once when we generate the code, and replace it by the constant value 9. If the expression appears in a loop, we save time each trip around the loop, and if the loop runs enough times, we will win back the overhead it took to study the expression and generate code for it. The key idea is that the notation gives us a general way to express a problem, but the compiler for the notation can customize the code for the details of the specific calculation. For example, in a virtual machine for regular expressions. we would likely have an operator to match a literal character: i n t matchchar(i n t 1i t e r a l
C
.
char * t e x t )
r e t u r n * t e x t == l i t e r a l ;
When we generate code for a particular pattern, however, the value of a given 1i t e r a l is fixed, say ' x ', so we could instead use an operator like this: i n t matchx(char * t e x t )
C r e t u r n * t e x t == ' x ' ;
3 And then, rather than predefining a special operator for each literal character value, we make things simpler by generating the code for the operators we really need for the current expression. Generalizing the idea for the full set of operations, we can write an on-the-fly compiler that translates the current regular expression into special code optimized for that expression. Ken Thompson did exactly this for an implementation of regular expressions on the IBM 7094 in 1967. His version generated little blocks of binary 7094 instructions for the various operations in the expression, threaded them together, and then ran the resulting program by calling it, just like a regular function. Similar techniques can be applied to creating specific instruction sequences for screen updates in graphics systems, where there are so many special cases that it is more efficient to create dynamic code for each one that arises than to write them all out ahead of time or to include conditional tests in more general code. To demonstrate what is involved in building a real on-the-fly compiler would take us much too far into the details of a particular instruction set, but it is worth spending some time to show how such a system works. The rest of this section should be read for ideas and insight but not for implementation details. Recall that we left our virtual machine with a structure like this:
SECTION 9.7
Code code [NCODE] ; i n t stack[NSTACK] ; i n t stackp; i n t pc; /* program counter
...
*/
Tree *t; t = parse() ; pc = generate(0, t) ; code [pcl .op = NULL;
stackp = 0; PC = 0; while (code [pc] .op != NULL) (*code [PC++] . op) (1 ; r e t u r n stack[O] ;
To adapt this code to on-the-fly compilation. we must make some changes. First, the code array is no longer an array of function pointers, but an array of executable instructions. Whether the instructions will be of type char, i n t . or long will depend on the processor we're compiling for; we'll assume i nt. After the code is generated, we call it as a function. There will be no virtual program counter because the processor's own execution cycle will walk along the code for us; once the calculation is done, it will return, like a regular function. Also, we can choose to maintain a separate operand stack for the machine or use the processor's own stack. Each approach has advantages, but we've chosen to stick with a separate stack and concentrate on the details of the code itself. The implementation now looks like this: typedef i n t Code; Code code [NCODEI ; i n t codep; in t stack [NSTACK] ; i n t stackp;
.. -
Tree n t ; voi d (*fn) (void) ; i n t pc; t = parse0 ; pc = generate(0, t) ; /* generate f u n c t i o n r e t u r n sequence */ genreturn(pc) ; stackp = 0; /* synchronize memory w i t h processor */ flushcaches() ; f n = (void(*)(void)) code; /a cast array t o p t r t o func */ (*f n) 0 ; /n c a l l f u n c t i o n n/ r e t u r n stack[O] ;
After generate finishes, genreturn lays down the instructions that make the generated code return control to eval . The function f l ushcaches stands for the steps needed to prepare the processor for running freshly generated code. Modem machines run fast in part because they have
244
NOTATION
CHAPTER 9
caches for instructions and data, and internal pipelines that overlap the execution of many successive instructions. These caches and pipelines expect the instruction stream to be static; if we generate code just before execution, the processor can become confused. The CPU needs to drain its pipeline and flush its caches before it can execute newly generated instructions. These are highly machine-dependent operations; the implementation of fl ushcaches will be different on each particular type of computer. The remarkable expression (voi d(n) (voi d)) code is a cast that converts the address of the array containing the generated instructions into a function pointer that can be used to call the code as a function. Technically, it's not too hard to generate the code itself, though there is a fair amount of engineering to do so efficiently. We start with some building blocks. As before, a code array and an index into it are maintained during compilation. For simplicity, we'll make them both global, as we did earlier. Then we can write a function to lay down instructions: /n emit: append i n s t r u c t i o n t o code stream void emit (Code i nst)
*/
C code Ccodep++] = in s t ;
1 The instructions themselves can be defined by processor-dependent macros or tiny functions that assemble the instructions by filling in the fields of the instruction word. Hypothetically, we might have a function called popreg that generates code to pop a value off the stack and store it in a processor register, and another called pushreg that generates code to take the value stored in a register and push it onto the stack. Our revised addap function would use them like this, given some defined constants that describe the instructions (like ADDINST) and their layout (the various SHIFT positions that define the format):
/* addop: generate ADD i n s t r u c t i o n v o i d addopcvoi d)
*/
r
Code i n s t ; popreg(2) ; /n pop stack i n t o r e g i s t e r 2 */ popreg(1) ; /n pop stack i n t o r e g i s t e r 1 n/ i n s t = ADDINST > right shift operator, 8, 135, 194 >>= assignment operator, 8 >>, Java logical right shift operator, 194 ?
questionable code notation. 2, 88 zero or one metacharacter, 223,228 ?: conditional operator, 8, 193 [I characterclass metacharacter, 223.228 \ line continuationcharacter, 240 quote metacharacter, 223,228 A start of string metacharacter. 222 { } braces, position of, 10
I OR metacharacter. 223 bitwise operator. 7. 127
I I logical operator, 6, 193 abort library function, 125 abstraction. 104,202 add function. Markov C, 68 addend list function. 46 addf ront list function. 46 addname list function, 42 addop function, 233,244 addsuff i x function, Markov C, 68 advquoted function. CSV, 97-98 Aho, Al, xii algorithm binary search, 31,52 constant-lime, 41,44.49,55.76 cubic, 41 exponential, 41 linear, 30.41.4647 logn, 32.41.51-52.76 Markov chain, 62-63 nlogn, 34.41 quadratic. 40.43, 176 quicksort, 32 sequential search, 30 tree sort. 53 alignment, 206 structure member, 195 a1loca function, 180 allocation error, memory. 130 memory. 48.67.92
allocator,special-purpose, 180. 182 ambiguity and parenthesization, 6 if-else. 10 analysis of algorithms, see 0-notation ANSVISO C standard. 190,212 any character metacharacter. ., 223 application program interface (API), 105. 198 apply list function. 47 appl yi norder tree function, 53 appl ypostorder tree function. 54 approximate values, 18 1 Ariane 5 rocket. 157 arithmetic IEEE floating-point. I 12. 181. 193 shift, 135. 194 Arnold, Ken, xii. 83 array bounds. 14 Array Java, 39 1ength field. Java, 22 array, s t a t i c . 131 *array[] vs. **array. 30 arrays.growing, 41-44.58.92.95.97. 158 ASCII encoding. 210 assembly language, 152. 181.237 a s s e r t macro, 142 < a s s e r t . h> header, 142 assignment multiple, 9 operator, =, 9, 13 operator. ww-. 8 associative array, see also hash table associative array, 78, 82 atexi t library function, 107 Austern, Matthew. 83 avg function. 141 Awk. 229 profile. 174 program, fmt, 229 program, Markov. 79 program. spl i t .awk, 229 test, 150 backwards compatibility, 209.2 1 1 balanced tree, 52. 76 benchmarking, 187 Bentley, Jon, xii, 59, 163, 188 beta release test, 160 Bigelow. Chuck, xii big-endian. 204,213 binary files, 132. 157.203 mode U0. 134.207 binary search algorithm, 3 1, 52 for error. 124
function. 1ookup. 31.36 testing, 146 tree, 50 tree diagram, 5 1 bi nhex program. 203 bi son compiler-compiler. 232 bi t b l t operator, 241 bitfields. 183. 191. 195 bitwise operator &, 7. 127 1. 7. 127 black box testing, 159 Bloch. Joshua, xii block, try, 113 Booth. Rick, 188 boundary condition testing. 140-141, 152. 159-160 Bourne. Steven R., 158 braces. position of I). 10 Brooks, Frederick P.. Jr., 6 1.83. 87. 115 bsearch library function. 36 B-tree, 54 buffer flush. 107, 126 overflow error, 67, 156157 buffering, U0. 180 bug, see also error bug environment dependent. 13 1 header file. 129 i s p r i n t , 129.136 list. 128 mental model. 127 non-reproducible. 130-13 1 performance, 18.82. 175 reports, 136 test program, 129 typographical, 128 bui 1d function Markov C, 67 Markov C++. 77 b y ~ order, e 194,204-207 diagram. 204 byteorder program, 205 C function prototype. 191 standard. ANSVISO. 190.212 C++ inline function. 17. 19 i o s t ream library. 77 s o r t function. 37 standard, ISO, 76, 190,212 s t r i n g class, 100 caching, 179, 186,243 can't get here message. 124 can't hoppen message. 15. 142. 155
INDEX
Cargill. Tom. xii carriage return, \r, 89.96.203-204 cast, 35.40.43.244 C/C++ preprocessor, see preprocessor directive C/C++ data type sizes. 192.2 16 cerr error stream. I26 Chain class. Markov Java, 72 Chain.add function. Markov Java, 73 Chai n . bui 1d function. Markov Java. 73 Chain. generate function, Markov Java, 74 character set. see encoding character class metacharacter, [I. 223. 228 characters HTML. 31 non-printing, 132 unsigned. 57. 152. 193 check function, 125 Christiansen.Tom, 83 ci n input stream, 77 class C++ string. 1 0 container, 7 I. 76 Csv. 100 Java Date, 172 Java Deci ma1Format, 22 1 Java Hashtable, 7 1 Java Random, 39 Java StreamTokenizer. 73 Java Vector, 71 Markov, 72 Markov Java Chain, 72 Markov Java Prefix. 72 Cleeland, Chris, xii clock library function, 171 CLOCKS-PER-SEC timer resolution, 172 clone method, see object copy Cmp interface, 38 code generation by macro, 240 Code structure, 234 code tuning. 176, 178-182 Cohen, Danny, 213 Coleridge, Samuel Taylor. 247 command echo, 207 interpreter, 106. 228 status return, 109.225 sum, 208 time, 171 comma-separated values, see also CSV comma-separated values. 8 6 8 7 comments, 2S27.203 semantic. 239 common subexpressionelimination, 178 Comparable interface, 37 compatibility. backwards, 209.21 1 compiler gcc. 120
just-in-time. 81,241,243 optimization, 176. 186 testing, 147,239 compiler-compiler bison, 232 yacc, 232.245 compile-timecontrol flow. 199 complex expressions, 7 complexity. 40 conditional compilation. 25. 199 operator. ?: 8. 193 configuration script, 20 1 conservation properties, testing, 147, 16 1 consistency, 4, 1 1, 105 const declaration, 20 constant-time algorithm, 41.44.49.55, 76 constructor, 100, 107-108 Markov Java Prefix, 74 container class. 7 1. 76 deque. 76.81 hash, 76.81 l i s t , 81 map, 72.76.81 pair, 112 vector. 76, 100 control flow, compile-time, 199 control-Z end of file. 134,207 convention --naming, 104 naming, 3-5. 104 conversionerror. pri ntf, 120 Cooper, Alan. 115 coordinate hashing. 57-58 copy, object, 67.73. 107-108, I6 1 cost model, performance, 184 Coughran, Bill, xii coverage, test, 148 Cox, Russ. xii CPU pipeline, 179,244 CRLF. 204 CSV advquoted function, 97-98 csvfield function, 98 csvnfi eld function. 98 endofl ine function. 96 main function. 89.98, 103 reset function, 96 s p l i t function. 97 field diagram, 95 forma, 91.93.96 in C. 91-99 in C++. 99-103 prototype, 87-91 specification, 93 "csv . h" header. 94
.
255
Csv: :advpl a i n function, 102 Csv: : advquoted function, 102 Csv: :endofline function. 101 CSV: :getfie1 d function. 102 Csv: :get1 i ne function, 100 Csv::g e t n f i e l d function, 102 Csv: : s p l i t function, 101 Csv class, 100 csvf i e l d function, CSV. 98 csvgetl i ne function, 95 prototype. 88 variables, 94 csvnf i e l d function. CSV, 98 ctime library function, 25, 144 header, 109 error message, see also epri ntf, wepri ntf error binary search for. 124 buffer overflow, 67, 156157 gets. 14, 156 handling, 109 hardware, 130 memory allocation. 130 message format, 114 message. misleading. 134 numeric patterns o f , 124 off-by-one. 13, 124. 141 order of evaluation, 9, 193 out of bounds. 153 patterns, 120 Pentium floating-point. 130 p r i n t f conversion, 120 q s o r t argument. 122 recent change, 120 recovery, 92, 109-113 reproducible. 123 return values, 91. 111, 141, 143 scanf, 120 status return, 109 stream, cerr, 126 stream, stderr, 104. 126 stream. System.err. 126 subscript out of range, 14, 140, 157 " errors. h" header, 238 estimation. performance. 184-187 estrdup function, 110, 114 eval function. 233-234, 236 evaluation eager. 181 expression. 233 lazy, 92.99 multiple. 18-19.22 of macro argument, multiple, 18. 129 examples. regular expression. 223,230,239 Excel spreadsheet. 97 exhaustive testing, 154 expected performance. 40 exponential algorithm. 41 expression, see also regular expression expression evaluation. 233 format, 7 style. 6-8 expressions complex. 7
negated, 6,s. 25 readability of, 6 extensions, printf, 216 fa1 l o c symbol, 5 fall-through. switch. 16 f a r pointer. 192 fdopen function, 134 f f l u s h library function. 126 f g e t s library function, 22.88.92. 140, 156 Fielding, Raymond, 29 file. see also header files binary, 132. 157.203 test data. 157 f i n a l declaration. 21 find library function, 30 find- fi rst-of library function, 101-102 Flandrena, Bob. xii. 188 f 1oat vs. doubl e, 183 floating-point arithmetic. IEEE. 112. I8 1. 193 error, Pentium. 130 flush, buffer, 107. 126 fmt Awk program, 229 f o r loop idioms, 12, 194 format CSV, 91,93,96 dynamic printf, 68 output, 89 p r i n t f %. *s, 133 string, p r i n t f , 216 Fraser, Chris, 245 f read library function, 106,205 free list. 180 free library function. 48 multiple calls of, 13 1 f reeal 1 list function, 48 French. Rente. xii f req program, 147. 16 1 Friedl. Jeffrey. 246 Frost. Robert, 85 fscanf library function, 67 function, see also library function function macros. see also macros function addend list, 46 addf ront list, 46 addname list. 42 addop, 233,244 a1 loca, 180 apply list, 47 appl yi norder tree. 53 appl ypostorder tree. 54 avg, 141 C++ inline. 17, 19
257
C++ sort, 37 check. 125 CSV advquoted, 97-98 CSV csvf ie l d, 98 CSV csvnfield. 98 CSV endofline, 96 CSV main. 89.98. 103 CSV reset, 96 CSV s p l i t , 97 Csv: :advplain 102 Csv::advquoted, 102 Csv: :endofline. 101 Csv: :getfield. 102 Csv: :getline. 100 csvgetline, 95 Csv: :getnfield. 102 Csv: : s p l i t , 101 d e l i tem list, 49 delname, 43 divop. 236 emall oc, 46, 110 emit, 244 eprintf, 49, 109 estrdup. 110, 114 eval. 233-234.236 fdopen, 134 f reeall list, 48 generate, 235 getbits, 183 grep, 226 grep main, 225 Icmp Integer comparison, 38 icmp integer comparison, 36 inccounter list, 48 i n s e r t tree, 51 isspam. 167. 169. 177 leftmost longest matchstar. 227 1ookup binary search, 31.36 lookup hash table, 56 1ookup list, 47 lookup tree, 52 macro, isoctal, 5 macros, 17-19 Markov C add, 68 Markov C addsuffix, 68 Markov C bui 1d. 67 Markov C++ bui 1d. 77 Markov C generate, 70 Markov C++ generate, 78 Markov C hash. 66 Markov C lookup, 67 Markov C main, 7 1 Markov C++ main. 77 Markov Java Chain . add, 73 Markov Java Chain.bui 1d. 73 Markov Java Chain . generate, 74 Markov Java main, 72 Markov Java Prefix . equal s, 75
Markov Java Prefix. hashcode, 74 match, 224 matchhere, 224 matchstar, 225 memset. 152 names, 4 newi tem list, 45 nrlookup tree, 53 nvcmp name-value comparison, 37 pack, 218 pack-typel. 217,219 parameter. . . . ellipsis. 109, 218 pointer, 34.47, 122,220-221.233, 236,244 p r i ntnv list, 47 progname. 110 prototype, C, 191 pushop, 236 quicksort. 33 Quicksort . rand, 39 Quicksort . sort. 39 Quicksort. swap, 39 receive. 221 Scmp String comparison, 38 scmp string comparison, 35 setprogname. 110 strdup. 14,110. 196 strings, 132 strings main, 133 s t r s t r , 167 swap, 33 testmalloc, 158 unpack, 219 unpack-type2. 220 unquote. 88 usage. 114 vinual, 221 weprintf, 52, 109. 197 wrapper, 111 f w r i t e library function, 106, 205 Gamma, Erich, 84 garbage collection, 48.75. 108 reference count, 108 gcc compiler, 120 generate function, 235 Markov C, 70 Markov C++. 78 generic class. see container class getbi t s function, 183 getchar idioms, 13, 194 library function, 13, 194 getquotes. t c l Tcl program, 87 gets error, 14, 156 library function, 14, 156 geturl . t c l Tcl program, 230 GIF encoding. 184
global variable, 3.24, 104. 122 Gosling, James, 83,212 got here message. 124 graph of hash table chains, 126 hash table size. 174 grep function, 226 implementation. 225-227 mai n function. 225 options, 228 program, 223-226 Grosse, Eric, xii growing arrays, 414.58.92.95.97. 158 hash table. 58 Hanson, David 115,245 Harbison, Sam, 212 hardware error, 130 hash function, 55-57 function, Java, 57 function multiplier, 56-57 table. 55-58.78. 169 table chains, graph of. 126 table diagram, 55 table function, 1ookup. 56 table, growing, 58 table insenion. 56 table. prefix, 64 table size. 56-57.65 table size, graph of, 174 value. 55 hash container, 76.81 function, Markov C, 66 hashing. coordinate, 57-58 Hashtabl e class. Java. 71 header < a s s e r t . hz, 142 "csv . h", 94 . 18.21. 129,210 " e p r i n t f . h", 110 <errno. h>, 109 "errors.h", 238 <stdarg.h>, 109.218 xstddef . h>, 192 <stdio. h,, 104, 196 <stdl i b. h>, 198 , 171 header file bug, 129 organization, 94 Helm, Richard, 84 Hemingway, Ernest, 63 Hennessy. John. 188
Herron, Andrew. xii hexadecimal output. 125 histogram, 126 Hoare, C. A. R.. 32, 37 holes in structure, 195 Holzmann, Gerard, xii, 57.59 homoiousian vs. homoousian, 228 hot spot, 130, 172-1 74 HTML, 86, 157,215,230,237 characters, 31 HTTP. 89.204 Icmp Integer comparison function, 38 i cmp integer comparison function. 36 idioms, 1&17 f o r loop. 12, 194 getchar, 13, 194 infinite loop, 12 list uaversal. 12 loop. 12-13. 140 malloc, 14 memnove array update, 43.68 new, 14 real 1oc, 43.95 side effects, 195 string copy, 14 suing truncation. 26 switch, 16 idle loop, 177 IEEE floating-point arithmetic, 1 12, 181, 193 # i f preprocessordirective, 196 #i fdef, see also conditional compilation # i f def preprocessordirective, 25, 196. 198-201 i f-el s e ambiguity, 10 i nccounter list function, 48 increment operator. ++, 9 incremental testing, 145 indentation style, 6, 10. 12, 15 independent implementations,testing by. 148 i ndexOf Java library function. 30 Inferno operating system, 181,210,213 infinite loop idioms, 12 information hiding, 92.99, 104.202 in C, 94.103 initialization,static, 99, 106 inline function. C++, 17, 19 in-order tree uaversal, 53 input mode, rb, 134,207 sueam, cin, 77 stream, stdin, 104 i n s e r t tree function. 51 insenion, hash table. 56 insuuctions. stack machine. 235 integer comparison function, i cmp, 36 overflow. 36. 157
interface Cmp, 38 Comparable, 37 principles. 91, 103-106 Seri a1i zabl e, 207 interface. Java. 38 interfaces, user. 1 13-1 15 internationalization, 209-2 11 interpreter, 231,234 intersection, portability by, 198 YO binary mode, 134,207 buffering, 180 text mode, 134 IOException. 113 iostream library, C++, 77 i s a l pha library function, 210 IS0 10646 encoding. 31.210 C++ standard. 76. 190.2 12 i soctal function macro, 5 i s p r i n t bug, 129, 136 i sspam function, 167, 169, 177 i supper library function, 18-21 i suppercase Java library function, 21 Java Array, 39 Array 1ength field, 22 data type sizes, 193 Date class. 172 Deci ma1 Format class, 22 1 hash function, 57 Hashtable class. 71 interface, 38 library function, Date. getTi me, 172 library function, i ndexof, 30 library function, i suppercase, 21 library function, Math. abs. 39 logical right shift operator, >>>, 194 Object. 38.40.71 quicksort, 37-40 Random class, 39 random library function, 24, 162 StreamTokeni zer class. 73 synchroni zed declaration, 108 Vector class. 71 Vinual Machine. 237 JavaScript. 2 15 JIT, see just-in-time compiler Johnson, Ralph, 84 Joy, Bill, 212 just-in-time compiler. 81,241.243 Kernighan. Brian. 28,212,245 Kernighan, Mark, xii key, search, 36.55.77
Knuth, Donald, 59. 159. 162, 172. 188.245 Koenig, Andy, xii, 239 Lakos. John, xii, 115 language eqn, 229 lawyer, 191 mainstream, 191 standard, 190 languages scripting, 80, 82, 230 testing, 150 Latin- l encoding. 210 lazy evaluation, 92.99 leap year computation, 7, 1 I , 144 leftmost longest match, 226 matchstar function, 227 1 ength field, Java Array, 22 library C++ i o s t ream, 77 design, 9 1-94 son. 34-37 library function abort. 125 a t e x i t , 107 bsearch, 36 clock, 171 ctime, 25, 144 Date.getTime Java, 172 fflush, 126 fgets, 22.88.92. 140, 156 find, 30 find- fi rst-of, 101-102 f read, 106,205 free, 48 fscanf, 67 fwri te, 106,205 getchar, 13, 194 gets. 14, 156 i ndex0f Java, 30 isalpha. 210 i supper, 18.21 i suppercase Java, 21 Java random, 24, 162 longjmp. 113 malloc, 14, 120, 131, 157 Math .abs Java. 39 memcmp, 173 memcpy. 43. 105 memmove. 43.68. 105 memset, 182 new, 14, 120 qsort, 34 rand. 33.70 real loc, 43,95, 120 scanf. 9, 156. 183 setbuf, setvbuf, 126
setjmp, 113 setmode, 134 s p r i n t f . 67 s t r c h r . 30. 167 strcmp, 26 strcpy. 14 strcspn, 97, 101, 155 s t r e r r o r , 109, 112 s t r l e n . 14 strncmp, 167 s t r s t r . 30, 167 s t r t o k . 88.96, 105, 108, 155 v f p r i n t f , 109 Linderman, John, xii Lindholm, Tim, 245 line continuation character. \, 240 linear algorithm, 30.41.4647 search, 30.32 list bug. 128 diagram. 45 doubly-linked, 49, 81 function, addend, 46 function. addf ront. 46 function, addname, 42 function, apply, 47 function, d e l i tem. 49 function, f reeal 1, 48 function, i nccounter, 48 function. lookup, 47 function. newi tem, 45 function, printnv. 47 representation. 45-46.49 singly-linked. 4 5 traversal idioms, 12 1 i s t container. 81 lists, 44-50 literate programming, 240 little languages, 151, 216,229 little-endian, 204 local variable, 3, 122 pointer to, 130 Locanthi, Bart. 241. 246 log file, 111, 125, 131 logical 6, 193 operator. &&. operator. l I, 6, 193 right shift operator. >>> Java, 194 shift, 135, 194 logn algorithm, 32.41.51-52.76 longjmp library function. 113 1ookup binary search function, 31.36 function. Markov C , 67 hash table function, 56 list function, 47 tree function, 52
loop do-while, 13, 133.225 eliminarion, 179 idioms. 12-13, 140 inversion. 169 LOOP macro. 240 loop unrolling. 179 variable declaration, 12 machine stack, 234 virtual, 203,213,232.236 machine-dependent code, 181 macro. 17-19 argument, multiple evaluation of, 18, 129 a s s e r t , 142 code generation by, 240 LOOP, 240 NELEMS. 22.3 1 va-arg,va-list. va-start, va-end. 109,218 magic numbers. 2. 19-22. 129 Maguire, Steve. 28. 137 main function CSV, 89.98. 103 grep, 225 Markov C . 71 Markov C u , 77 Markov Java, 72 s t r i n g s , 133 mainstream. language, 191 ma1 1oc debugging. 13 1 idioms, 14 library function. 14, 120. 131. 157 management memory, 48 resource, 92. 106-109 map container, 72.76. 81 Markov Awk program. 79 C add function, 68 C addsuffix function. 68 C bui 1 d function. 67 C++ bui 1d function, 77 C generate function, 70 C++ generate function, 78 C hash function. 66 C lookup function. 67 C main function. 71 C++ main function. 77 chain algorithm, 6 2 4 3 data structure diagram. 66 hash table diagram, 66 Java Chain class, 72 Java Chai n. add function. 73 Java Chain. bui 1d function. 73 Java Chain .generate function, 74
Java main function, 72 Java P r e f i x class, 72 Java P r e f i x constructor, 74 Java P r e f i x . equal s function. 75 Java P r e f i x . hashcode function, 74 Per1 program, 80 program testing. 160-162 run-time table, 8 1 state, 64 t e s t program, 161 Markov class, 72 Mars Pathfinder, 121 Marx Brothers, 253 match, leftmost longest, 226 match function, 224 matchhere function. 224 matchstar function, 225 leftmost longest. 227 Math. abs Java library function, 39 McConnell, Steve. 28. 115. 137 McIlroy, Doug, xii, 59 McNamee, Paul. xii mechanization, 86, 146, 149, 155, 237-240 memcmp library function. 173 memcpy library function, 43, 105 Memishian. Peter, xii memmove array update idioms. 43,68 library function, 43.68, 105 memory allocator. see ma1 1oc, new memory allocation. 48.67.92 allocation error. 130 leak, 107, 129. 131 management, 48 memset function, 152 library function. 182 test, 152-153 mental model bug. 127 message, see also e p r i n t f , wepri n t f message can 'f gef here, 124 can'f happen. 15, 142, 155 format, error, 114 gof here, 124 metacharacter . any character, 223 [I character class. 223,228 % end of suing, 222 + one or more, 223,228 I OR. 223 \ quote, 223,228 A start of string, 222 * zero or more, 223.225.227 ? zero or one, 223,228
metacharacters Perl. 231 regular expression, 222 MIMEencoding. 203 Minnie, A.. 253 misleading error message. 134 Mitchell, Don P., 82 Modula-3, 237 Mullender, Sape, xii Mullet. Kevin. 115 multiple assignment, 9 calls of f r e e . 13 1 evaluation, 18-19.22 evaluation of macro argument, 18, 129 multiplier, hash function, 56-57 multi-threading, 90, 108. 118 multi-way decisions, 14 names descriptive, 3 function, 4 variable. 3-4, 155 Nameval structure, 3 1.42.45. 50, 55 name-value structure, see Nameval structure name-value comparison function, nvcmp, 37 naming convention, 3-5, 104 104 NaN not a number, 112 near pointer, 192 negated expressions, 6.8.25 NELEMS macro, 22. 31 Nelson. Peter, xii Nemeth, Evi, xii new idioms, 14 library function, 14. 120 newi tem list function. 45 n logn algorithm, 34.41 non-printing characters, 132 non-reproducible bug, 130-1 3 1 NONWORD value. 69 not a number. NaN, 112 notation for zero, 2 1 printf-like. 87.99. 217 nrlookup tree function, 53 nu11 byte, '\0', 21 NULL pointer. 21 nu1 1 reference, 2 1.73 numbers. magic, 2. 19-22, 129 numeric patterns of error, 124 numerology, 124 nvcmp name-value comparison function, 37 NVtab structure. 42
--.
object copy, 67.73, 107-108. 161
Object. Java, 38,40.71 off-by-oneerror, 13, 124, 141 one or more metacharacter, +. 223.228 0-notation, see also algorithm 0-notation, 40-41 table, 41 on-the-fly compiler, see just-in-timecompiler opaque type, 104 operating system Inferno, l8I.2lO.213 Plan 9, 206.210.213.238 virtual, 202,213 operator & bitwise, 7. 127 && logical, 6, 193 ++ increment, 9 = assignment. 9. 13 >> right shift, 8, 135, 194 >>= assignment, 8 >>> Java logical right shift, 194 ? : conditional, 8, 193 I bitwise, 7, 127 I I logical. 6. 193 b i t b l t , 241 function table, optab, 234 overloading. 100, 183 precedence. 6-7, 127 relational, 6, 127 s i zeof. 22, 192, 195 optab operator function table, 234 optimization, compiler, 176, 186 options, grep, 228 OR metacharacter. 1, 223 order of evaluation error. 9. 193 organization. header file, 94 out of bounds error, 153 output debugging, 123 format, 89 hexadecimal, 125 stream, stdout. 104 overflow, integer. 36, 157 overloading, operator, 100. 183
pack function, 218 pack-type1 function, 217,219 packet format diagram, 216 pack, unpack, 216-221 pai r container, 112 parameter, . . . ellipsis function, 109,218 parameters. default, 100 parentheses. redundant, 6 parenthesization, 18 and ambiguity, 6 parse tree, 54,232 diagram, 54,232
parser generator, see compiler-compiler pattern matching, see regular expression patterns, error, 120 Patterson, David, 188 Pentium floating-point error, 130 performance bug, 18,82, 175 cost model, 184 estimation. 184-187 expected, 40 graph, 126, 174 test suite, 168 worst-case, 40 Per1 metacharacters, 231 program, enum. pl. 239 program, Markov. 80 program, unhtml . pl, 230 regular expression. 230 test suite, 162 picture, see diagram Pike, Rob, 2 13.245-246 pipeline. CPU, 179,244 pivot element, quicksort. 32-34 Plan 9 operating system, 206.210.213.238 Plauger, P. J., 28 pointer dangling. 130 f a r , 192 function, 34.47. 122,22C!-22 1,233,236,244 near. 192 NULL, 21 to local variable, 130 void*, 21.43.47 portability, 189 by intersection, 198 by union. 198 position of {I braces, 10 POSIX standard, 198.21 2 post-condition, 141 post-order tree traversal, 54,232 Postscript. 203,215,237,239 PPM encoding, 184 Pracrice of Programming web page. xi precedence,operator, 6-7, 127 pre-condition. 141 Prefix class, Markov Java. 72 constructor, Markov Java, 74 prefix hash table. 64 Prefix .equals function, Markov Java, 75 Prefix. hashcode function, Markov Java, 74 pre-order tree traversal, 54 preprocessordirective #def i ne, 2.20.240 # e l i f . 199 #endif. 199
#if, 196 #ifdef, 25, 196, 198-201 Presotto. David, 2 13 principles, interface, 91, 103-1015 pri n t f conversion error. 120 extensions, 216 format. dynamic, 68 format string, 216 %.*s format, 133 pri ntf-like notation, 87.99.217 printnv list function. 47 production code. 83.99 profile Awk, 174 spam filter, 173-174 profiling. 167, 172-174 progname function, 110 program byteorder, 205 counter, 236, 243 enum. pl Perl, 239 fmt Awk. 229 freq. 147. 161 getquotes . t c l Tcl. 87 geturl . t c l Tcl, 230 grep. 223-226 inverse. 147 Markov Awk. 79 Markov Perl. 80 Markov t e s t , 161 sizeof. 192 s p l i t . awk Awk. 229 strings, 131-134 unhtml . pl Perl, 230 vis, 134 programmable tools, 228-23 1 programming, defensive, 114. 142 protocol checker, Supenrace, 57 prototype code. 83.87 CSV, 87-91 csvgetl i ne, 88 pushop function, 236 qsort argument error. 122 library function, 34 quadratic algorithm, 40.43. 176 questionable code notation, ?, 2, 88 quicksort algorithm. 32 analysis, 34 diagram. 33 Java, 37-40 pivot element, 32-34 quicksort function, 33
Quicksort. rand function, 39 Quicksort. s o r t function, 39 Quicksort. swap function, 39 quoternetacharacter, \, 223. 228 quotes, stock, 86 \ r carriage return, 89.96.203-204 Rabinowitz, Many. xii rand library function, 33. 70 Random class. Java, 39 random selection, Ilk, 70 random library function, Java, 24, 162 rb input mode. 134.207 readability of expressions, 6 real 1 oc idioms, 43.95 library function. 43.95, 120 receive function. 221 recent change error. 120 records. test. 15 1 recovery, error, 92, 109-113 reduction in strength, 178 redundant parentheses, 6 reentrant code, 108 reference argument, I l l , 220 null, 21.73 reference count garbage collection. 108 regression testing, 149 regular expression, 99.222-225.239.242 examples. 223.230.239 metacharacters. 222 Perl, 230 Tcl, 230 Reiser, John. 246 relational operator, 6. 127 representation list, 45-46, 49 sparse matrix. 183 tree, 50 two's complement, 194 reproducible error, 123 r e s e t function, CSV. 96 resource management. 92.106-109 return, see carriage return right shift operator. >>. 8, 135. 194 operator. >>> Java logical, 194 Ritchie. Dennis, xii. 212-2 13 Sam text editor. 202, 213 Sano, Darrell. 115 scanf error. 120 library function. 9, 156, 183 Schwartz, Randal. 83 Scmp S t r i n g comparison function. 38
scmp suing comparison function, 35 script configuration, 201 test. 149. 160 scripting languages. 80.82.230 search algorithm, sequential, 30 key, 36.55.77 searching, 3C-32 Sedgewick, Roben, 59 selection, Ilk random, 70 self-checking code. 125 self-contained test, 150 semantic comments. 239 sentinel, 30.69-71 sequential search algorithm, 30 Serial i zabl e interface. 207 setbuf. setvbuf library function, 126 setjmp library function, 113 setmode library function. 134 setprogname function. 110 Shakespeare. William, 165 Shaney, Mark V.. xii, 84 shell, see command interpreter Shneiderman, Ben, 115 side effects, 8-9, 18, 193 idioms. 195 signals. 197 single point of truth. 238 singly-linked list, 45 size, hash table. 56-57.65 s i ze-t type, 192. 199 s i zeof operator, 22, 192, 195 program. 192 sizes ClC++ data type. 192,216 Java data type, 193 son algorithm, tree, 53 library, 34-37 s o r t function, C++, 37 sorting strings. 35 source code control, 12 1, 127 space efficiency. 182-184 spam filter, 166-170 data structure diagram, 170 profile, 173-174 sparse matrix representation, 183 special-case tuning, 181 special-purpose allocaror, 180, 182 specification, 87.93 csv, 93 s p l i t function, CSV, 97 spl i t.awk Awk program. 229 spreadsheet format, see comma-sepamtt:d values
spreadsheet, 139.22 1 Excel, 97 s p r i n t f library function, 67 stack machine, 234 machine instructions, 235 trace. 118-1 19. 122 standard ANSIIISO C, 190,212 I S 0 C++. 76. 190,212 language. 190 POSIX, 198,212 Standard Template Library. see STL start of string metachamcter, A, 222 state. Markov, 64 S t a t e structure, 65 static initialization, 99. 106 static array. 131 declaration, 94 statistical test, 161 status return command, 109,225 error, 109 <stdarg. h> header, 109.218 <stddef. h> header. 192 s t d e r r error stream. 104. 126 s t d i n input stream, 104 <stdio. h> header. 104. 196 <stdl i b. h> header. 198 stdout output stream. 104 Steele,Guy, 212 Stevens. Rich, xii, 212 STL, 49.76, 104. 155. 192 stock quotes, 86 Strachey. Giles Lytton, 215 s t r c h r library function, 30, 167 strcmp library function. 26 strcpy library function, 14 strcspn library function, 97. 101, 155 strdup function. 14, 110, 196 StreamTokeni zer class, Java, 73 s t r e r r o r library function, 109, 112 stress testing, 155-159.227 string copy idioms. see also strdup string comparison function, scmp. 35 copy idioms, 14 truncation idioms. 26 s t r i n g class. C++, 100 strings function, 132 main function. 133 program, 131-134 s t r l en library function, 14 strncmp library function, 167 Strousuup, Bjarne. xii, 83
strstr function, 167 implementation, 167-168 library function, 30, 167 s t r t o k library function, 88.96. 105, 108, 155 structure Code, 234 holes in, 195 member alignment. 195 Nameval, 3 1.42.45.50.55 NVtab, 42 S t a t e , 65 Suffix, 66 Symbol, 232 Tree. 233 Strunk. William, 1, 28 style expression. 6-8 indentation, 6, 10. 12, 15 subscript out of range error. 14. 140. 157 suffix, 62 S u f f i x structure. 66 sum command, 208 Supertrace protocol checker. 57 swap function. 33 Swift, Jonathan, 213 switch fall-through, 16 idioms, 16 Symbol structure. 232 symbol table, 55.58 synchronized declaration, Java, 108 syntax tree, see parse tree System. e r r error stream, 126 Szymanski. Tom, xii table Markov run-time, 81 0-notation, 41 optab operator function, 234 tail recursion. 53 Taylor. Ian Lance, xii Tcl program, getquotes. t c l . 87 program. g e t u r l . t c l , 230 regular expression, 230 teddy bear, 123. 137 lest Awk, 150 beta release, 160 coverage, 148 data files, 157 memset, 152-153 program bug. 129 records, 151 scaffold, 89.98, 146, 149, 151-155 script, 149, 160
self-contained, 150 statistical, 161 suite, performance, 168 suite, Perl, 162 t e s t program, Markov, 161 testing binary search, 146 black box, 159 boundary condition, 14C-141, 152, 159-160 by independent implementations, 148 compiler, 147,239 conservation properties, 147, 161 exhaustive, 154 incremental. 145 languages. 150 Markov program, 160-162 regression, 149 stress, 155-1 59,227 tools, 147, 149 white box. 159 testmal l o c function. 158 text mode YO, 134 Thimbleby, Harold, 115 Thompson, Ken, xii, 188,213,242,246 threaded code, 234 time command. 171 header. 171 timer resolution. CLOCKS-PER-SEC 172 tools programmable, 228-23 1 testing, 147, 149 Toyama. Kentaro, xii tradeoffs, design, 90 Traveling Salesman Problem. 41 uee, 5C-54,231-237 balanced, 52.76 binary search, 50 function, appl yi norder, 53 function, appl ypostorder, 5 4 function. i n s e r t , 51 function, lookup, 52 function, nrlookup, 5 3 parse. 54, 232 representation, 50 sort algorithm, 53 Tree structure, 233 tree traversal in-order. 53 post-order, 54.232 pre-order. 54 Trickey. Howard. xii, 213 uie data structure, 17 1 TRIP test for TEX, 159, 162 t r y block, 113 tuning code, 176, 178-182 special-case. 181
tuple. 112 two's complement representation, 194 type derived, 38 opaque. 104 s i ze-t, 192, 199 typedef declaration, 76.2 17 typographical bug, 128 unhtml . pl Per1 program, 230 Unicode encoding, 31.210.228 uninitialized variables, 120, 159 union, portability by, 198 unpack funct~on,219 unpack-type2 function, 220 unquote function, 88 unsigned characters, 57, 152, 193 usage function. 114 user interfaces, 113-1 15 USS Yorkfown, 142 UTF-8 encoding, 21 1,213,228 uuencode, uudecode. 203 va-arg, v h l i st, v h s t a r t , v h e n d macro. 109.218 values. error return, 91, 11 1, 141. 143 van der Linden, Peter, 28 Van Wyk, Chris, xii variable errno, 112.193 global, 3,24, 104, 122 local, 3. 122 names. 3-4, 155 variables csvgetl i ne. 94 uninitialized, 120, 159 Vector class, Java, 71 v e c t o r container, 76, 100 Venturi, Roben, 189 v f p r i n t f library function, 109
virtual function. 221 machine, 203,213,232.236 operating system, 202,213 v i s program, 134 Visual Basic, 2 15,237 Vlissides, John, 84 void* pointer, 2 1.43.47 Wadler, Phil, xii Wait, John W.. xii Wall, Larry. 83 Wang, Daniel C., xii warning message, see wepri ntf web browser. 86.23 1 page, Practice of Programming, xi Weinberger. Peter. xii weprintf function, 52, 109, 197 white box testing, 159 White, E. B.. 1.28 wide characters, 211 Wiener, Norben, 139 wildcards, *, 106,222 Winterbottom. Philip, 213 worst-case performance. 40 wrapper function, I 11 Wright, Margaret, xii
X Window system, 202,206 yacc compiler-compiler. 232,245 Year 2000 problem, 144, 182 Yellin, Frank, 245 Yorkfown, 142 Young. Cliff, xii zero, 21 division by, 141-142. 236,241 notation for, 2 1 zero or more metacharacter. *, 223,225, 227 zero or one metacharacter. ?. 223.228