OSDN Git Service

Announce 0.0.9.1 bugfix release.
[android-x86/external-toybox.git] / www / design.html
1 <b><h2>Design goals</h2></b>
2
3 <p>Toybox should be simple, small, and fast.  Often, these things need to be
4 balanced off against each other.  In general, simple is slightly more
5 important than small, and small is slightly more important than fast, but
6 it should be possible to get 80% of the way to each goal before they really
7 start to fight.</p>
8
9 <b><h3>Fast</h3></b>
10
11 <p>It's easy to say lots about optimizing for speed (which is why this section
12 is so long), but at the same time it's the one we care the least about.
13 The essence of speed is being as efficient as possible, which means doing as
14 little work as possible.  A design that's small and simple gets you 90% of the
15 way there, and most of the rest is either fine-tuning or more trouble than
16 it's worth (and often actually counterproductive).  Still, here's some
17 advice:</p>
18
19 <p>First, understand the darn problem you're trying to solve.  You'd think
20 I wouldn't have to say this, but I do.  Trying to find a faster sorting
21 algorithm is no substitute for figuring out a way to skip the sorting step
22 entirely.  The fastest way to do anything is not to have to do it at all,
23 and _all_ optimization boils down to avoiding unnecessary work.</p>
24
25 <p>Speed is easy to measure; there are dozens of profiling tools for Linux
26 (although personally I find the "time" command a good starting place).
27 Don't waste too much time trying to optimize something you can't measure,
28 and there's no much point speeding up things you don't spend much time doing
29 anyway.</p>
30
31 <p>Understand the difference between throughput and latency.  Faster
32 processors improve throughput, but don't always do much for latency.
33 After 30 years of Moore's Law, most of the remaining problems are latency,
34 not throughput.  (There are of course a few exceptions, like data compression
35 code, encryption, rsync...)  Worry about throughput inside long-running
36 loops, and worry about latency everywhere else.  (And don't worry too much
37 about avoiding system calls or function calls or anything else in the name
38 of speed unless you are in the middle of a tight loop that's you've already
39 proven isn't running fast enough.)</p>
40
41 <p>"Locality of reference" is generally nice, in all sorts of contexts.
42 It's obvious that waiting for disk access is 1000x slower than doing stuff in
43 RAM (and making the disk seek is 10x slower than sequential reads/writes),
44 but it's just as true that a loop which stays in L1 cache is many times faster
45 than a loop that has to wait for a DRAM fetch on each iteration.  Don't worry
46 about whether "&" is faster than "%" until your executable loop stays in L1
47 cache and the data access is fetching cache lines intelligently.  (To
48 understand DRAM, L1, and L2 cache, read Hannibal's marvelous ram guid at Ars
49 Technica:
50 <a href=http://arstechnica.com/paedia/r/ram_guide/ram_guide.part1-2.html>part one</a>,
51 <a href=http://arstechnica.com/paedia/r/ram_guide/ram_guide.part2-1.html>part two</a>,
52 <a href=http://arstechnica.com/paedia/r/ram_guide/ram_guide.part3-1.html>part three</a>,
53 plus this
54 <a href=http://arstechnica.com/articles/paedia/cpu/caching.ars/1>article on
55 cacheing</a>, and this one on
56 <a href=http://arstechnica.com/articles/paedia/cpu/bandwidth-latency.ars>bandwidth
57 and latency</a>.
58 And there's <a href=http://arstechnica.com/paedia/index.html>more where that came from</a>.)
59 Running out of L1 cache can execute one instruction per clock cycle, going
60 to L2 cache costs a dozen or so clock cycles, and waiting for a worst case dram
61 fetch (round trip latency with a bank switch) can cost thousands of
62 clock cycles.  (Historically, this disparity has gotten worse with time,
63 just like the speed hit for swapping to disk.  These days, a _big_ L1 cache
64 is 128k and a big L2 cache is a couple of megabytes.  A cheap low-power
65 embedded processor may have 8k of L1 cache and no L2.)</p>
66
67 <p>Learn how virtual memory and memory managment units work.  Don't touch
68 memory you don't have to.  Even just reading memory evicts stuff from L1 and L2
69 cache, which may have to be read back in later.  Writing memory can force the
70 operating system to break copy-on-write, which allocates more memory.  (The
71 memory returned by malloc() is only a virtual allocation, filled with lots of
72 copy-on-write mappings of the zero page.  Actual physical pages get allocated
73 when the copy-on-write gets broken by writing to the virtual page.  This
74 is why checking the return value of malloc() isn't very useful anymore, it
75 only detects running out of virtual memory, not physical memory.  Unless
76 you're using a NOMMU system, where all bets are off.)</p>
77
78 <p>Don't think that just because you don't have a swap file the system can't
79 start swap thrashing: any file backed page (ala mmap) can be evicted, and
80 there's a reason all running programs require an executable file (they're
81 mmaped, and can be flushed back to disk when memory is short).  And long
82 before that, disk cache gets reclaimed and has to be read back in.  When the
83 operating system really can't free up any more pages it triggers the out of
84 memory killer to free up pages by killing processes (the alternative is the
85 entire OS freezing solid).  Modern operating systems seldom run out of
86 memory gracefully.</p>
87
88 <p>Also, it's better to be simple than clever.  Many people think that mmap()
89 is faster than read() because it avoids a copy, but twiddling with the memory
90 management is itself slow, and can cause unnecessary CPU cache flushes.  And
91 if a read faults in dozens of pages sequentially, but your mmap iterates
92 backwards through a file (causing lots of seeks, each of which your program
93 blocks waiting for), the read can be many times faster.  On the other hand, the
94 mmap can sometimes use less memory, since the memory provided by mmap
95 comes from the page cache (allocated anyway), and it can be faster if you're
96 doing a lot of different updates to the same area.  The moral?  Measure, then
97 try to speed things up, and measure again to confirm it actually _did_ speed
98 things up rather than made them worse.  (And understanding what's really going
99 on underneath is a big help to making it happen faster.)</p>
100
101 <p>In general, being simple is better than being clever.  Optimization
102 strategies change with time.  For example, decades ago precalculating a table
103 of results (for things like isdigit() or cosine(int degrees)) was clearly
104 faster because processors were so slow.  Then processors got faster and grew
105 math coprocessors, and calculating the value each time became faster than
106 the table lookup (because the calculation fit in L1 cache but the lookup
107 had to go out to DRAM).  Then cache sizes got bigger (the Pentium M has
108 2 megabytes of L2 cache) and the table fit in cache, so the table became
109 fast again...  Predicting how changes in hardware will affect your algorithm
110 is difficult, and using ten year old optimization advice and produce
111 laughably bad results.  But being simple and efficient is always going to
112 give at least a reasonable result.</p>
113
114 <p>The famous quote from Ken Thompson, "When in doubt, use brute force",
115 applies to toybox.  Do the simple thing first, do as little of it as possible,
116 and make sure it's right.  You can always speed it up later.</p>
117
118 <b><h3>Small</h3></b>
119 <p>Again, simple gives you most of this.  An algorithm that does less work
120 is generally smaller.  Understand the problem, treat size as a cost, and
121 get a good bang for the byte.</p>
122
123 <p>Understand the difference between binary size, heap size, and stack size.
124 Your binary is the executable file on disk, your heap is where malloc() memory
125 lives, and your stack is where local variables (and function call return
126 addresses) live.  Optimizing for binary size is generally good: executing
127 fewer instructions makes your program run faster (and fits more of it in
128 cache).  On embedded systems, binary size is especially precious because
129 flash is expensive (and its successor, MRAM, even more so).  Small stack size
130 is important for nommu systems because they have to preallocate their stack
131 and can't make it bigger via page fault.  And everybody likes a small heap.</p>
132
133 <p>Measure the right things.  Especially with modern optimizers, expecting
134 something to be smaller is no guarantee it will be after the compiler's done
135 with it.  Binary size isn't the most accurate indicator of the impact of a
136 given change, because lots of things get combined and rounded during
137 compilation and linking.  Matt Mackall's bloat-o-meter is a python script
138 which compares two versions of a program, and shows size changes in each
139 symbol (using the "nm" command behind the scenes).  To use this, run
140 "make baseline" to build a baseline version to compare against, and
141 then "make bloatometer" to compare that baseline version against the current
142 code.</p>
143
144 <p>Avoid special cases.  Whenever you see similar chunks of code in more than
145 one place, it might be possible to combine them and have the users call shared
146 code.  (This is the most commonly cited trick, which doesn't make it easy.)</p>
147
148 <p>Some specific advice: Using a char in place of an int when doing math
149 produces significantly larger code on some platforms (notably arm),
150 because each time the compiler has to emit code to convert it to int, do the
151 math, and convert it back.  Bitfields have this problem on most platforms.
152 Because of this, using char to index a for() loop is probably not a net win,
153 although using char (or a bitfield) to store a value in a structure that's
154 repeated hundreds of times can be a good tradeoff of binary size for heap
155 space.</p>
156
157 <b><h3>Simple</h3></b>
158
159 <p>Complexity is a cost, just like code size or runtime speed.  Treat it as
160 a cost, and spend your complexity budget wisely.</p>
161
162 <p>Simplicity has lots of benefits.  Simple code is easy to maintain, easy to
163 port to new processors, easy to audit for security holes, and easy to
164 understand.  (Comments help, but they're no substitute for simple code.)</p>
165
166 <p><a href=http://www.joelonsoftware.com/articles/fog0000000069.html>Joel
167 Spolsky argues against throwing code out and starting over</a>, and he has
168 good points: an existing debugged codebase contains a huge amount of baked
169 in knowledge about strange real-world use cases that the designers didn't
170 know about until users hit the bugs, and most of this knowledge is never
171 explicitly stated anywhere except in the source code.</p>
172
173 <p>That said, the Mythical Man-Month's "build one to throw away" advice points
174 out that until you've solved the problem you don't properly understand it, and
175 about the time you finish your first version is when you've finally figured
176 out what you _should_ have done.  (The corrolary is that if you build one
177 expecting to throw it away, you'll actually wind up throwing away two.  You
178 don't understand the problem until you _have_ solved it.)</p>
179
180 <p>Joel is talking about what closed source software can afford to do: Code
181 that works and has been paid for is a corporate asset not lightly abandoned.
182 Open source software can afford to re-implement code that works, over and
183 over from scratch, for incremental gains.  Before toybox, the unix command line
184 has already been reimplemented from scratch several times in a row (the
185 original AT&amp;T Unix command line in assembly and then in C, the BSD
186 versions, the GNU tools, BusyBox...) but maybe toybox can do a better job. :)</p>
187
188 <p>P.S.  How could I resist linking to an article about
189 <a href=http://blog.outer-court.com/archive/2005-08-24-n14.html>why
190 programmers should strive to be lazy and dumb</a>?</p>
191
192 <b><h2>Portability issues</h2></b>
193
194 <b><h3>Platforms</h3></b>
195 <p>Toybox should run on every hardware platform Linux runs on.  Other
196 posix/susv3 environments (perhaps MacOS X or newlib+libgloss) are vaguely
197 interesting but only if they're easy to support; I'm not going to spend much
198 effort on them.</p>
199
200 <p>I don't do windows.</p>
201
202 <b><h3>32/64 bit</h3></b>
203 <p>Toybox should work on both 32 bit and 64 bit systems.  By the end of 2008
204 64 bit hardware will be the new desktop standard, but 32 bit hardware will
205 continue to be important in embedded devices for years to come.</p>
206
207 <p>Toybox relies on the fact that on any Unix-like platform, pointer and long
208 are always the same size (on both 32 and 64 bit).  Pointer and int are _not_
209 the same size on 64 bit systems, but pointer and long are.</p>
210
211 <p>This is guaranteed by the LP64 memory model, a Unix standard (which Linux
212 and MacOS X both implement).  See
213 <a href=http://www.unix.org/whitepapers/64bit.html>the LP64 standard</a> and
214 <a href=http://www.unix.org/version2/whatsnew/lp64_wp.html>the LP64
215 rationale</a> for details.</p>
216
217 <p>Note that Windows doesn't work like this, and I don't care.
218 <a href=http://blogs.msdn.com/oldnewthing/archive/2005/01/31/363790.aspx>The
219 insane legacy reasons why this is broken on Windows are explained here.</a></p>
220
221 <b><h3>Signedness of char</h3></b>
222 <p>On platforms like x86, variables of type char default to unsigned.  On
223 platforms like arm, char defaults to signed.  This difference can lead to
224 subtle portability bugs, and to avoid them we specify which one we want by
225 feeding the compiler -funsigned-char.</p>
226
227 <p>The reason to pick "unsigned" is that way we're 8-bit clean by default.</p>