In computer science, algorithmic efficiency is a property of an algorithm which relates to the number of computational resources used by the algorithm. An algorithm must be analyzed to determine its resource usage, and the efficiency of an algorithm can be measured based on usage of different resources. Algorithmic efficiency can be thought of as analogous to engineering productivity for a repeating or continuous process.
For maximum efficiency we wish to minimize resource usage. However, different resources such as time and space complexity cannot be compared directly, so which of two algorithms is considered to be more efficient often depends on which measure of efficiency is considered most important.
For example, bubble sort and timsort are both algorithms to sort a list of items from smallest to largest. Bubble sort sorts the list in time proportional to the number of elements squared (, see Big O notation), but only requires a small amount of extra memory which is constant with respect to the length of the list (). Timsort sorts the list in time linearithmic (proportional to a quantity times its logarithm) in the list's length (), but has a space requirement linear in the length of the list (). If large lists must be sorted at high speed for a given application, timsort is a better choice; however, if minimizing the memory footprint of the sorting is more important, bubble sort is a better choice.
Background
The importance of efficiency with respect to time was emphasised by Ada Lovelace in 1843 as applying to Charles Babbage's mechanical analytical engine:
"In almost every computation a great variety of arrangements for the succession of the processes is possible, and various considerations must influence the selections amongst them for the purposes of a calculating engine. One essential object is to choose that arrangement which shall tend to reduce to a minimum the time necessary for completing the calculation"
Early electronic computers
were severely limited both by the speed of operations and the amount of
memory available. In some cases it was realized that there was a space–time trade-off, whereby a task
could be handled either by using a fast algorithm which used quite a
lot of working memory, or by using a slower algorithm which used very
little working memory. The engineering trade-off was then to use the
fastest algorithm which would fit in the available memory.
Modern computers are significantly faster than the early computers, and have a much larger amount of memory available (Gigabytes instead of Kilobytes). Nevertheless, Donald Knuth emphasised that efficiency is still an important consideration:
"In established engineering disciplines a 12% improvement, easily obtained, is never considered marginal and I believe the same viewpoint should prevail in software engineering"
Overview
An
algorithm is considered efficient if its resource consumption, also
known as computational cost, is at or below some acceptable level.
Roughly speaking, 'acceptable' means: it will run in a reasonable
amount of time or space on an available computer, typically as a function
of the size of the input. Since the 1950s computers have seen dramatic
increases in both the available computational power and in the available
amount of memory, so current acceptable levels would have been
unacceptable even 10 years ago. In fact, thanks to the approximate doubling of computer power every 2 years, tasks that are acceptably efficient on modern smartphones and embedded systems may have been unacceptably inefficient for industrial servers 10 years ago.
Computer manufacturers frequently bring out new models, often with higher performance.
Software costs can be quite high, so in some cases the simplest and
cheapest way of getting higher performance might be to just buy a faster
computer, provided it is compatible with an existing computer.
There are many ways in which the resources used by an algorithm
can be measured: the two most common measures are speed and memory
usage; other measures could include transmission speed, temporary disk
usage, long-term disk usage, power consumption, total cost of ownership, response time
to external stimuli, etc. Many of these measures depend on the size of
the input to the algorithm, i.e. the amount of data to be processed.
They might also depend on the way in which the data is arranged; for
example, some sorting algorithms perform poorly on data which is already sorted, or which is sorted in reverse order.
In practice, there are other factors which can affect the
efficiency of an algorithm, such as requirements for accuracy and/or
reliability. As detailed below, the way in which an algorithm is
implemented can also have a significant effect on actual efficiency,
though many aspects of this relate to optimization issues.
Theoretical analysis
In the theoretical analysis of algorithms,
the normal practice is to estimate their complexity in the asymptotic
sense. The most commonly used notation to describe resource consumption
or "complexity" is Donald Knuth's Big O notation, representing the complexity of an algorithm as a function of the size of the input . Big O notation is an asymptotic measure of function complexity, where roughly means the time requirement for an algorithm is proportional to , omitting lower-order terms that contribute less than to the growth of the function as grows arbitrarily large. This estimate may be misleading when is small, but is generally sufficiently accurate when is large as the notation is asymptotic. For example, bubble sort may be faster than merge sort
when only a few items are to be sorted; however either implementation
is likely to meet performance requirements for a small list. Typically,
programmers are interested in algorithms that scale
efficiently to large input sizes, and merge sort is preferred over
bubble sort for lists of length encountered in most data-intensive
programs.
Some examples of Big O notation applied to algorithms' asymptotic time complexity include:
Notation | Name | Examples |
---|---|---|
constant | Finding the median from a sorted list of measurements; Using a constant-size lookup table; Using a suitable hash function for looking up an item. | |
logarithmic | Finding an item in a sorted array with a binary search or a balanced search tree as well as all operations in a Binomial heap. | |
linear | Finding an item in an unsorted list or a malformed tree (worst case) or in an unsorted array; Adding two n-bit integers by ripple carry. | |
linearithmic, loglinear, or quasilinear | Performing a Fast Fourier transform; heapsort, quicksort (best and average case), or merge sort | |
quadratic | Multiplying two n-digit numbers by a simple algorithm; bubble sort (worst case or naive implementation), Shell sort, quicksort (worst case), selection sort or insertion sort | |
exponential | Finding the optimal (non-approximate) solution to the travelling salesman problem using dynamic programming; determining if two logical statements are equivalent using brute-force search |
Benchmarking: measuring performance
For new versions of software or to provide comparisons with competitive systems, benchmarks are sometimes used, which assist with gauging an algorithms relative performance. If a new sort algorithm
is produced, for example, it can be compared with its predecessors to
ensure that at least it is efficient as before with known data, taking
into consideration any functional improvements. Benchmarks can be used
by customers when comparing various products from alternative suppliers
to estimate which product will best suit their specific requirements in
terms of functionality and performance. For example, in the mainframe world certain proprietary sort products from independent software companies such as Syncsort compete with products from the major suppliers such as IBM for speed.
Some benchmarks provide opportunities for producing an analysis
comparing the relative speed of various compiled and interpreted
languages for example
and The Computer Language Benchmarks Game compares the performance of implementations of typical programming problems in several programming languages.
Even creating "do it yourself"
benchmarks can demonstrate the relative performance of different
programming languages, using a variety of user specified criteria. This
is quite simple, as a "Nine language performance roundup" by Christopher
W. Cowell-Shah demonstrates by example.
Implementation concerns
Implementation
issues can also have an effect on efficiency, such as the choice of
programming language, or the way in which the algorithm is actually
coded, or the choice of a compiler for a particular language, or the compilation options used, or even the operating system being used. In many cases a language implemented by an interpreter may be much slower than a language implemented by a compiler. See the articles on just-in-time compilation and interpreted languages.
There are other factors which may affect time or space issues,
but which may be outside of a programmer's control; these include data alignment, data granularity, cache locality, cache coherency, garbage collection, instruction-level parallelism, multi-threading (at either a hardware or software level), simultaneous multitasking, and subroutine calls.
Some processors have capabilities for vector processing, which allow a single instruction to operate on multiple operands;
it may or may not be easy for a programmer or compiler to use these
capabilities. Algorithms designed for sequential processing may need to
be completely redesigned to make use of parallel processing, or they could be easily reconfigured. As parallel and distributed computing grow in importance in the late 2010s, more investments are being made into efficient high-level APIs for parallel and distributed computing systems such as CUDA, TensorFlow, Hadoop, OpenMP and MPI.
Another problem which can arise in programming is that processors compatible with the same instruction set (such as x86-64 or ARM)
may implement an instruction in different ways, so that instructions
which are relatively fast on some models may be relatively slow on other
models. This often presents challenges to optimizing compilers, which must have a great amount of knowledge of the specific CPU
and other hardware available on the compilation target to best optimize
a program for performance. In the extreme case, a compiler may be
forced to emulate instructions not supported on a compilation target platform, forcing it to generate code or link an external library call
to produce a result that is otherwise incomputable on that platform,
even if it is natively supported and more efficient in hardware on other
platforms. This is often the case in embedded systems with respect to floating-point arithmetic, where small and low-power microcontrollers
often lack hardware support for floating-point arithmetic and thus
require computationally expensive software routines to produce floating
point calculations.
Measures of resource usage
Measures are normally expressed as a function of the size of the input .
The two most common measures are:
- Time: how long does the algorithm take to complete?
- Space: how much working memory (typically RAM) is needed by the algorithm? This has two aspects: the amount of memory needed by the code (auxiliary space usage), and the amount of memory needed for the data on which the code operates (intrinsic space usage).
For computers whose power is supplied by a battery (e.g. laptops and smartphones), or for very long/large calculations (e.g. supercomputers), other measures of interest are:
- Direct power consumption: power needed directly to operate the computer.
- Indirect power consumption: power needed for cooling, lighting, etc.
As of 2018, power consumption is growing as an important metric for computational tasks of all types and at all scales ranging from embedded Internet of things devices to system-on-chip devices to server farms. This trend is often referred to as green computing.
Less common measures of computational efficiency may also be relevant in some cases:
- Transmission size: bandwidth could be a limiting factor. Data compression can be used to reduce the amount of data to be transmitted. Displaying a picture or image (e.g. Google logo) can result in transmitting tens of thousands of bytes (48K in this case) compared with transmitting six bytes for the text "Google". This is important for I/O bound computing tasks.
- External space: space needed on a disk or other external memory device; this could be for temporary storage while the algorithm is being carried out, or it could be long-term storage needed to be carried forward for future reference.
- Response time (latency): this is particularly relevant in a real-time application when the computer system must respond quickly to some external event.
- Total cost of ownership: particularly if a computer is dedicated to one particular algorithm.
Time
Theory
Analyze the algorithm, typically using time complexity
analysis to get an estimate of the running time as a function of the
size of the input data. The result is normally expressed using Big O notation.
This is useful for comparing algorithms, especially when a large amount
of data is to be processed. More detailed estimates are needed to
compare algorithm performance when the amount of data is small, although
this is likely to be of less importance. Algorithms which include parallel processing may be more difficult to analyze.
Practice
Use a benchmark to time the use of an algorithm. Many programming languages have an available function which provides CPU time usage.
For long-running algorithms the elapsed time could also be of interest.
Results should generally be averaged over several tests.
Run-based profiling can be very sensitive to hardware
configuration and the possibility of other programs or tasks running at
the same time in a multi-processing and multi-programming environment.
This sort of test also depends heavily on the selection of a
particular programming language, compiler, and compiler options, so
algorithms being compared must all be implemented under the same
conditions.
Space
This section is concerned with the use of memory resources (registers, cache, RAM, virtual memory, secondary memory) while the algorithm is being executed. As for time analysis above, analyze the algorithm, typically using space complexity
analysis to get an estimate of the run-time memory needed as a function
as the size of the input data. The result is normally expressed using Big O notation.
There are up to four aspects of memory usage to consider:
- The amount of memory needed to hold the code for the algorithm.
- The amount of memory needed for the input data.
- The amount of memory needed for any output data.
- Some algorithms, such as sorting, often rearrange the input data and don't need any additional space for output data. This property is referred to as "in-place" operation.
- The amount of memory needed as working space during the calculation.
- This includes local variables and any stack space needed by routines called during a calculation; this stack space can be significant for algorithms which use recursive techniques.
Early electronic computers, and early home computers, had relatively small amounts of working memory. For example, the 1949 Electronic Delay Storage Automatic Calculator (EDSAC) had a maximum working memory of 1024 17-bit words, while the 1980 Sinclair ZX80 came initially with 1024 8-bit bytes of working memory. In the late 2010s, it is typical for personal computers to have between 4 and 32 GB of RAM, an increase of over 300 million times as much memory.
Caching and memory hierarchy
Current computers can have relatively large amounts of memory
(possibly Gigabytes), so having to squeeze an algorithm into a confined
amount of memory is much less of a problem than it used to be. But the
presence of four different categories of memory can be significant:
- Processor registers, the fastest of computer memory technologies with the least amount of storage space. Most direct computation on modern computers occurs with source and destination operands in registers before being updated to the cache, main memory and virtual memory if needed. On a processor core, there are typically on the order of hundreds of bytes or fewer of register availability, although a register file may contain more physical registers than architectural registers defined in the instruction set architecture.
- Cache memory is the second fastest and second smallest memory available in the memory hierarchy. Caches are present in CPUs, GPUs, hard disk drives and external peripherals, and are typically implemented in static RAM. Memory caches are multi-leveled; lower levels are larger, slower and typically shared between processor cores in multi-core processors. In order to process operands in cache memory, a processing unit must fetch the data from the cache, perform the operation in registers and write the data back to the cache. This operates at speeds comparable (about 2-10 times slower) with the CPU or GPU's arithmetic logic unit or floating-point unit if in the L1 cache. It is about 10 times slower if there is an L1 cache miss and it must be retrieved from and written to the L2 cache, and a further 10 times slower if there is an L2 cache miss and it must be retrieved from an L3 cache, if present.
- Main physical memory is most often implemented in dynamic RAM (DRAM). The main memory is much larger (typically gigabytes compared to ≈8 megabytes) than an L3 CPU cache, with read and write latencies typically 10-100 times slower. As of 2018, RAM is increasingly implemented on-chip of processors, as CPU or GPU memory.
- Virtual memory is most often implemented in terms of secondary storage such as a hard disk, and is an extension to the memory hierarchy that has much larger storage space but much larger latency, typically around 1000 times slower than a cache miss for a value in RAM. While originally motivated to create the impression of higher amounts of memory being available than were truly available, virtual memory is more important in contemporary usage for its time-space tradeoff and enabling the usage of virtual machines. Cache misses from main memory are called page faults, and incur huge performance penalties on programs.
An algorithm whose memory needs will fit in cache memory will be much
faster than an algorithm which fits in main memory, which in turn will
be very much faster than an algorithm which has to resort to virtual
memory. Because of this, cache replacement policies are extremely important to high-performance computing, as are cache-aware programming and data alignment.
To further complicate the issue, some systems have up to three levels
of cache memory, with varying effective speeds. Different systems will
have different amounts of these various types of memory, so the effect
of algorithm memory needs can vary greatly from one system to another.
In the early days of electronic computing, if an algorithm and
its data wouldn't fit in main memory then the algorithm couldn't be
used. Nowadays the use of virtual memory appears to provide lots of
memory, but at the cost of performance. If an algorithm and its data
will fit in cache memory, then very high speed can be obtained; in this
case minimizing space will also help minimize time. This is called the principle of locality, and can be subdivided into locality of reference, spatial locality and temporal locality.
An algorithm which will not fit completely in cache memory but which
exhibits locality of reference may perform reasonably well.
Criticism of the current state of programming
- David May FRS a British computer scientist and currently Professor of Computer Science at University of Bristol and founder and CTO of XMOS Semiconductor, believes one of the problems is that there is a reliance on Moore's law to solve inefficiencies. He has advanced an 'alternative' to Moore's law (May's law) stated as follows:
Software efficiency halves every 18 months, compensating Moore's Law
- May goes on to state:
In ubiquitous systems, halving the instructions executed can double the battery life and big data sets bring big opportunities for better software and algorithms: Reducing the number of operations from N x N to N x log(N) has a dramatic effect when N is large ... for N = 30 billion, this change is as good as 50 years of technology improvements.
- Software author Adam N. Rosenburg in his blog "The failure of the Digital computer", has described the current state of programming as nearing the "Software event horizon", (alluding to the fictitious "shoe event horizon" described by Douglas Adams in his Hitchhiker's Guide to the Galaxy book). He estimates there has been a 70 dB factor loss of productivity or "99.99999 percent, of its ability to deliver the goods", since the 1980s—"When Arthur C. Clarke compared the reality of computing in 2001 to the computer HAL 9000 in his book 2001: A Space Odyssey, he pointed out how wonderfully small and powerful computers were but how disappointing computer programming had become".