I’m going to assume this question is about ahead-of-time compilers; JIT compilers are a different story altogether. With that in mind…
The performance benefits of single-pass compilers are overstated
For starters, there is essentially no longer any benefit to implementing a single-pass compiler: they are a relic of an era in which memory was extraordinarily scarce, and compilers simply could not afford to fit the entire program in memory; this has not been true for 30 years. This is really a crucial point: single-pass compilers were developed to save on memory, not time. Fundamentally, they do all the same work as an equally-simple multi-pass compiler (modulo some marginal translation costs), they just run the whole pipeline on one statement or definition at a time rather than on entire compilation units.
Perhaps you feel that memory usage is nothing to sneeze at. After all, compilers—and especially optimizing compilers—can use quite a lot of memory, and though memory may be much cheaper than it once was, it’s still not free. But this is something of a red herring: compilers that do things that need lots of memory cannot be implemented as single-pass compilers because…
Single-pass compilers just can’t do the job
Modern compilers have features and perform analyses that require considering much more context than a single definition at a time. At least some of these features are present in essentially all modern programming languages. Let’s consider some examples (though this list is not by any means exhaustive).
Forward references without separate forward declarations are almost universally supported by modern programming languages. There is essentially no reason to statically forbid forward references in general.
Parse errors and typechecking failures can be made significantly more helpful if the compiler has access to the entire module when generating them. For example, suppose the user has written a correct definition, but given it the wrong type. A naïve compiler would trust the type declaration and report several type mismatches: one at the definition and another at every use site.
However, if the compiler has access to the use sites, it could note that all the uses reflect the actual type rather than the expected one. It can therefore include a suggestion in the error message that perhaps the type signature ought to be changed.
Different languages perform differing amounts of type inference, but some languages, like Haskell, have type inference that is quite global. In Haskell, a definition’s type is determined by both its body and all of its uses, so computing the type of a top-level definition may require typechecking all other definitions in the module.
GHC (the Haskell compiler) does break up modules into declaration groups very early in the pipeline, immediately after resolving names, based on dependency analysis between declarations (which is very cheap). This allows the definition groups to be typechecked independently, so users don’t pay a cost for this feature if they don’t use it.
Essentially all ahead-of-time compilers worth their salt perform some very basic optimizations, even if they don’t do enough to warrant the name “optimizing compiler”. For example, most compilers will do constant folding and constant propagation, reducing constant expressions like
5 * 2 to
10. And once any optimizations are at play, a crucial part of the process is inlining.
In order to inline a function, its body must be known to the compiler while optimizing its call site. This is naturally impossible if the compiler has already processed its code and promptly unloaded it from memory. So compilers generally want to keep the definitions of functions from the same module in memory, anyway, and many even perform cross-module inlining for sufficiently small functions, which requires keeping them in memory, too.
More sophisticated optimizing compilers also perform a battery of optimizations that are explicitly interprocedural, which is to say they cross function boundaries. For example, a compiler might notice that a program boxes some values and passes them to a function that immediately unboxes them, in which case it could remove the unnnecessary boxing. It might also keep track of what side-effects a function performs so that it can reorder calls to it if the compiler deems that will improve performance.
These sorts of optimizations fundamentally depend on considering much more than a single function at a time. In theory, some of them could be done by analyzing each function as it is compiled and saving the results of those analyses for downstream calls (and indeed this is how some cross-module interprocedural analysis works), but even where this is possible, it would miss out on some useful optimizations. This is because these optimizers are usually iterative: they perform an analysis, use it to make some improvements, then analyze the program again, since the previous step might have uncovered new opportunities for further optimization. This iterative process benefits greatly from larger compilation units where more information is available.
Common subexpression elimination
Optimizing compilers also perform common subexpression elimination (CSE), which essentially deduplicates code. Obviously, the more code the compiler is able to consider at once, the greater likelihood that it will find duplicate occurrences of the same thing.
Multi-pass compilers are much easier to write
As the above examples hopefully demonstrate, modern compilers do a lot of things. They must handle complex languages with lots of features, they must detect misuses and mistakes and report them to users in a comprehensible fashion, and they must somehow translate the language into another (usually much simpler) one while preserving its semantics. Ultimately, compilers are written by humans, so they must be organized in a way that allows them to flexibly grow and evolve.
For this reason, modern compilers of general-purpose programming languages near-universally translate the program into some intermediate representation that is easier to work with. Optimizing compilers use several intermediate representations, each progressively lower level than the last. These intermediate representations are much smaller and more uniform languages than the ones written by humans, which dramatically cuts down on the number of cases the compiler must consider.
In theory, this approach is not incompatible with processing a single definition at a time: a compiler could each single definition through the entire pipeline before moving onto the next one. But, as alluded to in the first paragraph of this answer, this would not save anything that wouldn’t also be saved by writing a simpler compiler! Sure, it would avoid needing to store the whole compilation unit in memory at once, but even an extraordinarily inefficient AST is not going to take up much more memory to store than the text file containing its source code. The real costs come from all the work the compiler is doing.
So unless you’re either writing a compiler for an exceptionally simple language or a compiler that must, for some reason, run on embedded systems, single-pass compilers are a relic of the past. Multi-pass compilers can do much more while remaining easier to understand, easier to maintain, easier to extend, and easier to debug. They’re a nearly universal compiler technique, and for good reason.