You could mail me or go to my home page, or the main squirrely page.

Compilers vs. Assemblers

squirrel with acorn

There has been a great debate on the use of assembly language and high-level languages continuing for years. Fanatics on both sides have insisted that one or the other is mostly useless. The truth, of course, is that both have their places.

I probably should give my biases up front. I am a professional software developer, and what I am mainly concerned about is how to produce good software efficiently, now and in the future. I anticipate that good software will last a long time, and therefore I place a high value on adaptability and portability. I am also concerned with professional growth. In any fast-moving field, any professional needs to be concerned with continuing education as well as fundamental knowledge and wisdom. If you want a view from another source, consider The Great Debate.

These are the main issues:

  1. Introduction
  2. Performance
  3. Optimizations
  4. Ease of Use
  5. Maintenance
  6. Portability
  7. Educational Value
  8. Conclusion

1. Introduction

Assembly language is a way of telling the computer exactly what to do, as specifically as possible. It compiles directly to "machine code", which is the most exact method of controlling an existing computer. All languages compile to machine code, except for interpreted ones, which are therefore less in control.

There is nothing conceptually fundamental in machine code or assembly language, though. It will be translated into gate controls inside the computer, and these will be executed by large collections of transistors. The only distinguishing feature is the practical: if you want to control the computer as precisely as you can, use assembly language. (Any computer scientist or software engineer should learn some assembly language sometime, just as such a person should learn some languages that are object- oriented, functional, and declarative. It is very useful to know the full range of tools available.)

The question is when to use assembly language, as opposed to other computer languages with less detailed control, but which are easier to use, and have other desirable characteristics.

Most computer languages in modern use are designed to present some sort of intermediate view between machine code and natural language. These languages need to be precise specifications of what is to be done, in some sense, easily understandable by a computer program and by humans. These languages usually have compilers that transform them into machine code (alternatively, there is a special program that fakes it as it goes along, at a cost in performance). The compilers are programs that take a program in a given computer language and spit it out into machine code. Since this is done mechanically, it is more reliable and less creative than human-written assembly language. There are good and bad points about that.

2. Performance

There are two useful definitions of performance here: program performance, considered here, or programmer performance, considered later. The question is here how to get the most performance out of a computer, and some of the tradeoffs involved.

It is obvious that the way to get the most efficient programs attainable is to write at least some assembly code by hand. There are things that computers do well, and things that humans do well. For the foreseeable future, humans have the deciding advantage that a human can look at compiler-generated code and twiddle it, whereas a compiler can't look at human-generated code and twiddle it.

On the other hand, this can be a lot of work. Most people don't want to do all of that to get the best performance, unless there is some strong reason why it is necessary. In some cases, there are performance requirements that are not easily reached any other way. In other cases, there are small routines that are known to be bottlenecks (perhaps simple graphic operations). In most cases, performance is not that much of an issue, and people can't be bothered to spend twice the time to do something closer to their very best.

Performance requirements are slippery things, anyway. Most computers are idle most of the time anyway, and making the programs they run faster will simply increase the amount of time they do nothing useful. There are exceptions, of course. Many applications are interactive, and in this case there is no need to process faster than the user can mouse or type. Computers are becoming faster all the time, so something that might be unpleasantly slow on last year's model may be very comfortable on next year's model. This applies to memory use also: both computer main memory and disk space are cheap, and the prices keep falling. Commercial programs are distributed on CD-ROMs, since this is the cheapest medium to mass-produce, and even a very bloated application will fit very comfortably on a CD-ROM. This isn't much of an imposition any more; a user who has to spend $100 upgrading a computer to run a $500 application is very likely to prefer that to a $600 application that does less on the current machine.

Having said that, there are very many situations in which performance does matter:

When considering performance, we have to consider cases in detail. Some computers are designed to be programmed in compiled languages, and have a simple, regular set of machine instructions (the "instruction set"). Others are designed to be programmed in assembly languages, and have more complex and irregular instruction sets. The most important of these computers use Intel's chips, which are hampered by having to maintain compatibility with previous computers (the Pentium Pro MMV is in a way compatible with the first true microprocessor). On a modern design, a good compiler is likely to produce more efficient machine instructions than a good assembly language programmer, unless the latter puts in great effort. On an Intel box, the human is likely to write code that runs perhaps twice as fast.

3. Optimizations

There are many different ways to make programs faster and more compact. I'll consider them from the large to the small, with attention to how handling these differs between compiled languages and assembly languages.

First, there is the general approach a program will take to solve a problem, generally called the "algorithm". Algorithms have been heavily studied, and in general a slow algorithm, no matter how well implemented, can reduce a computer system to a slow crawl. Since the study of algorithms is general in nature (lest the field become obsolete again every time an old computer model goes out of production), the choice of algorithms should be independent of the choice of language. The main difference is that compiled languages are designed to use the more well-known algorithms, and therefore it is easier to implement the more complex algorithms, as well as to understand them once implemented.

Second, there are machine-dependent optimizations. These are more difficult to generalize about, since machines (and hence their optimizations) vary wildly. These consist of taking advantage of the particular features of the machine. Some people seem to find these frequently, and some don't. I have done some strange things with the extremely quirky Z80 processor, but have found fewer opportunities in the more regular 68000 instruction set. In one case, I used the fact that the Z80 had two register sets to make very accurate arithmetic run very fast.

The disadvantage of these is that they are completely nonportable. If I had wanted that routine to run on any other microprocessor, I would have had to go back to the drawing board and start all over. The work involved in designing the algorithm, which was substantial, I could have used in any future system.

Third, it is possible to cut corners on standard practices. In the program I mentioned above, I also disabled interrupts and used the Z80 interrupt address register to hold a small value. Compilers, being basically mechanical, use a lot of "boilerplate" machine instructions, and adhere to certain conventions. These conventions vary between machine and machine, and sometimes between compilers. They always are fairly fast ways to link pieces of programs together, and they are robust. It is often possible to short-circuit the conventions, and in some cases this can make a program run much faster. It is likely that some functions calculate more than one thing at a time, and that a useful value is left in an easily accessible place; doing this can also increase program speed, sometimes, considerably. Doing either of these things makes it extremely difficult to change a program later, as the programmer has to track down every place where corners have been cut, and either restore the standard conventions or make sure the program still works. (The "useful value left in a register" trick can easily be done in higher-level languages, by using global variables. In fact, it is almost never done, which should say something about its usefulness.)

Fourth, modern computers are extremely complex, and can do many things at once. (It's much easier to make a computer that can do four things at once than to make one that can do one thing at a time, but twice as fast.) This means that the ordering of the machine instructions can be critical, since there are many possible orders, and each order will make the computer act in a different manner. Compilers are usually very good at picking out a good arrangement, but humans can do better if they work at it (if nothing else, the human knows what results are important, and which don't matter).

The effects of these optimizations can vary widely. The first is incredible; a fast algorithm can make an operation possible on a low-end personal computer that a slow (but not obviously wasteful) algorithm can make unusable on a supercomputer. Most of the time, though, people pick good algorithms (although there is an unaccountable urge to use bubblesort), and so this is not important in practice. The others are lesser speedups, although not negligible, and are important only in parts of the program that are used frequently.

In general, a computer program will consist of a lot of chunks that are run only once, a few that are not run at all in a given run, some that are run only a few times, and a few that run very frequently. Obviously, if you can speed pieces of a program up by a factor of two, doing to to a piece that's hardly used isn't going to be very useful. Not so obviously, people are very bad at spotting these "hot spots", and there is usually no reliable way of telling where they occur except to run the program with a "profiler". This means that piecemeal optimization is not likely to be worthwhile unless it is done after the program is actually running.

The size of the hot spots varies. Some people report that 10% of their program takes up about 90% of the execution time, some report that 20% takes up 80%. Some people find that the hot spots are fairly uniform among themselves, others find that the hot spots have hot spots, so a very small portion of a program might take up over half the execution time. In these cases, it may be worthwhile to optimize the small portion, but it is almost certainly not worthwhile to optimize any other spot.

Suppose that a function takes 100ms to run in, and we find that 20% of the function takes up 80% of the time, or 80ms. Obviously, the other 80% takes only 20ms, and if we were to speed that up to infinite speed we'd only reduce the function running time by 20%. Suppose we concentrate on that 20%, and suppose that we can get it to run twice as fast (perhaps reasonable on an Intel processor, probably unreasonable on a PowerPC) by translating it into assembly language. In that case, the hot 20% now takes 40ms to run, and the rest takes 20ms, so the hot spot still accounts for two-thirds of the run time. If we were to double the speed of the other 80% of the program, quintupling the optimization effort, we'd reduce the run time to 50ms. If we have to get the function running in 50ms, then we have to, and we'll put in the work. If we don't absolutely need to get it under 60ms, we generally won't bother.

4. Ease of Use

Pretty much every study done has concluded that programmers can write about the same amount of code in any language, measured in the number of language statements. Therefore, programmer productivity will be better in languages where one line of code does more than in languages where one line of code does less. Capers Jones has a list of programming languages giving the approximate number of assembler language statements a statement in a given language is worth. Note that languages vary wildly. A C statement is worth about 2.5 assembler language statements, whereas in programming in Common Lisp using the Common Lisp Object System, one statement is worth about 15 assembler language statements. (No, I don't know why more people don't code in Common Lisp, given this measure, and that's another argument entirely. Some people do. Go to the Association of Lisp Users implementation page and follow the corporate links to see that they do, indeed, have customers.)

The other main factor in productivity is the amount of pre-written code that is available. If a program has to do X, and there is a library function somewhere that will do X, that's money in the bank. By far the cheapest and most reliable code is the stuff that isn't there. There are very many libraries available, for free or for money, in all sorts of different languages. One difference is that the higher-level language libraries build up over the years from a user base varying over all sorts of machines, while the assembler-language libraries build up only from the users of one sort of machine (since a neat 6809 hack is useful to almost nobody nowadays).

5. Maintenance

No program lives in a vacuum (well, very few, anyway). Once a program is written, it may last for a long, long time (the year 2000 crisis is largely because many programs have lasted far longer than expected). User requirements change, largely because users are unable to figure out what they want until they have something close. Systems change. Software that has to work with a program is updated.

This isn't important for some programs. Many games are not updated, as the novelty value has worn off before they lose compatibility, and many programs are written for specific one-shot purposes. For most commercial programs that are not games, it is very important. Most productivity applications, for example, go through many updates during their lives, partly to add features, partly to maintain compatibility, and maintenance is a real money issue.

Now, it is possible to write maintainable code in any language, and it is possible to write crawling horrors in any language, so the question is about tendencies.

Higher-level languages are easier to write in, and easier to comprehend, precisely because they have fewer statements for any given amount of functionality. This applies for all things, and particularly to maintenance. If things are well laid-out, a higher-level language program will be easy to read, while an assembler-language program will be fairly easy. If things are badly written, the higher-level language program will be extremely difficult to maintain, while the assembler-language program will be impossible.

When surrounding software changes (the principal cause of "software rot"), the program we are working on will have to change, also. If we are using some sort of library, which is more likely with a higher-level language, it is likely that we simply have to update the library and recompile. If this isn't the case, or if it doesn't solve the problem entirely, the question is how much code needs to be changed. It is generally easier to encapsulate functionality in higher-level languages, and therefore generally easier to confine the amount of code that has to be changed.

We also need to consider the nature of optimizations. Some optimizations do not hinder maintainability, but most do. In particular, taking any sort of assembly language shortcut will make it much more difficult to change a program later; relying on any undocumented side effects, or non-standard procedure calls, will make the program a nightmare to cope with years later.

Finally, we need to consider that the hardware will change from under the program. I don't know how much we need to consider that; well- optimized 80386 code is almost certainly much better than C running on a Pentium Pro. Sixteen-bit 80x86 code is probably going to be a disaster. On the Macintosh, the move from the 680x0 series to the PowerPC series was a very, very big deal for assembler-language programs, and not serious at all for C and C++ programs. There may be a similar move in the Wintel world. While Intel has done a marvelous job in keeping the old x86 architecture running faster and faster, they are using an incredible amount of work to do so, and intuitively it seems that they can't keep it up indefinitely (although I would have predicted they couldn't keep it up this long). It seems likely that the Wintel world will eventually have to migrate to a new system architecture for performance reasons.

6. Portability

Portability is a complicated issue. A given program is likely to be written on a specific computer system, and possibly then moved to another one. Computer systems differ in the video displays used, the operating system calls, the structure of the file system, the instruction set, the amount of main memory, and so forth. Portability is not an absolute. Almost all programs can be ported from one system to another, and this usually takes some amount of work. In general, though, it is far easier to port programs written in a compiled language.

One basic difference is the instruction set. Assembly language is tied to a certain instruction set, and it is usually not possible to change instruction sets mechanically. In some cases, a manufacturer will go to great lengths to make it possible. When Intel introduced the 8086 and 8088 (the forerunners of the Pentium), they deliberately made them somewhat compatible with the less capable 8080 and 8085, but provided a special tool to translate 8085 assembly language into 8086/8088 machine instructions. When IBM introduced their personal computer, most of the available software was 8080 assembly language programs transformed in this way, and it ran slower on the IBM personal computer than on the lesser computers it was supplanting. (Most of this was rewritten fairly quickly.) Similar things happened with Motorola's introduction of the 6809 to replace the 6800. When Apple changed from 68040s to PowerPCs as the core of their Macintoshes, they provided a way for the PowerPC chips to run 68040 instructions. This was more successful, in that the PowerPC Macs tended to be at least as fast as their 68040 brethren running 68040 programs, but programs written for the PowerPC directly ran far faster. The difference between the performance of the 68040 and the PowerPC 601 was much greater than that between the 8085 and 8088.

Therefore, to move an assembly language program from a computer to one with a different instruction set, it is usually necessary to rewrite the whole thing. This is not, in general, overly difficult, but it can take a considerable amount of time.

To move a compiled program to a computer with a different instruction set, one recompiles it. It's likely to be more complicated than just that, since presumably there is a different compiler for a different computer, but for a program written portably in a higher-level language, there will be few more complications.

Most successful commercial programs nowadays have more than simple I/O, though. They tend to use the GUI abilities of whatever platform they're written for, and this requires a lot of work no matter what. This is not a mechanical process, since different platforms have different assumptions, capabilities, and styles. It is certainly possible to write a multi-platform interface, and this works well with games and other programs that are expected to be idiosyncratic. Beyond this, the approach never has caught on, although many people are now behind Sun's AWT interface for Java.

To see how important this is, we need to consider three things. How much of the time does this come up? How much of an effort is it? How much of the program is involved?

There are three primary GUI platforms right now: Microsoft Windows, the Macintosh, and X-Windows under Unix. The Macintosh operating systems is found on two different hardware platforms, the 680x0 series of processors and the PowerPC. Windows NT runs on Intel and DEC Alpha, and presumably would run again on the PowerPC if Microsoft thought it worth their while. X runs on a very large number of systems. When porting among these groups, moving from one system to another can be as simple as recompiling. When porting between these groups, expect a lot more work.

The next question is how much work compiler languages can save, and the answer is that it depends. Moving between major operating systems involves tearing out a lot of code and replacing it. There's no getting around that. Higher-level languages are presumably more expressive, so that you will get the same productivity benefits as you would in writing a new program. Further, you don't want to tear out and replace more code than is necessary, so ideally the system- dependent code will be in clearly defined places, and the non system- dependent code will be kept at a safe distance, so that it does not rely on the internals of the system-dependent code. Higher-level languages make it easier to hide internals than assembler languages.

Thirdly, much depends on how much of the program is GUI and how much is processing. A real-time action game is likely to be mostly display and interaction, and the best bet might be a complete rewrite. A spreadsheet will need a lot of user interface code, but things like the macro facilities and recalculation engines can easily be translated. A word processing program needs to do page layout and other things, and this is unlikely to be system-dependent. A database engine might do no user interaction by itself, and might be almost completely portable.

For these reasons, programs in higher-level languages are generally much more portable than assembler language programs.

7. Educational Value

Software professionals are working in a rapidly changing field. They have to find ways to keep up with new languages and methodologies. One of these ways is to learn the basics of computer science, so that the professional has a clue about anything new coming down the pike. Another way is to learn a lot of different things, so that anything new is likely to be similar to something the professional has learned before.

A professional should try to learn plenty of languages of different sorts. The types of higher-level languages that I am more or less familiar with are procedural, object-oriented, functional, and declarative. There are also specialized languages, such as SQL for database access. Some useful and fairly popular languages to learn would be C, C++, Fortran, Pascal, Basic, or Lisp (procedural), C++, Java, Lisp (object-oriented), Scheme or Lisp (functional), and Prolog (declarative). (This list is not intended to be complete, but if you can think of a language that is readily available across multiple platforms that I might mention, tell me about it.)

Another sort of language the professional should learn is assembler language, which should be considered a separate category. I would consider a professional with some practice in Fortran, Java, Scheme, Prolog, and at least one assembler language to be well-rounded in his or her experience. Which is more important depends on the individual, of course, but I would generally consider assembler as more important than declarative languages and less important than procedural languages.

Practically speaking, any developer is likely to get into tight spots that are best resolved by examining the compiler output, which is generally assembler language. It is likely that a developer might have to write some assembler language in some projects. The concepts of assembler language programming are not complicated, but the approach is different from most, so some experience is useful.

Assembler language has no further educational value, though. Some people seem to consider it as a fundamental, but I just don't see it. From a theoretical point of view, the assembler language system is an abstract machine, closer to the physical reality than higher level languages and farther from it than transistors. Its claim to notice is that it is typically the least abstract level that is easily manipulated using standard tools. This is not a fundamental distinction. Computer science is about bending computer processes to our will, and should be taught in an environment with as little irrelevant detail as possible, ideally in Scheme among the readily available languages. If we want to go into the messy details (and we will), assembler language is only one of many vehicles.

8. Conclusion

This leads us to a standard method of writing programs.

First, write it in whatever higher-level language you like. There are reasons to pick one language rather than another, but these depend on the individual project. It may be desirable to select a language that is familiar (or one that is unfamiliar, for educational purposes), available in convenient form, easily portable, easy to write programs in fast, easy to write fast programs in, what the boss tells you to write it in, whatever. Note that, if performance is important, it is vital to have some sort of profiling or metering tool available. The critically important thing is to know your algorithms and data structures. You may use algorithms that are not the most efficient, but you need to be aware of what you're doing.

There are uses for assembler language even in this stage. If there is a small, self-contained chunk of code that will have to be exceedingly fast, do whatever you have to. This applies to routines that you know will run thousands of times in a second, and that you know will be bottlenecks. There are routines of these sorts in graphics-based programs, and in other computationally intensive programs.

Once this is done, run the puppy. See how fast it is. Does it need more work? If so, find out where. Your guess is probably wrong. There will certainly be ways to speed up the program. Use them if appropriate. At first, keep to higher-level languages. It is likely that you will be changing things to see what's faster, or recasting things, and you need to keep the program flexible.

Eventually, either the program will be fast enough, or you will have exhausted this approach. In any case, keep this version of the program. It's the version you'll want as a launch pad if you're going to extend it, or port it, or whatever. If the program isn't fast enough, and isn't more than about a factor of two too slow (that's the best speedup you can expect), do whatever you have to to speed up the "hot spots".

That is the proper use of assembly language in modern software development.

All contents of these pages Copyright 1997 by David H. Thornley.