The reason that loops are faster than recursion is easy.
A loop looks like this in assembly.

mov loopcounter,i
dowork:/do work
dec loopcounter
jmp_if_not_zero dowork

A single conditional jump and some bookkeeping for the loop counter.

Recursion (when it isn't or cannot be optimized by the compiler) looks like this:

start_subroutine:
pop parameter1
pop parameter2
dowork://dowork
test something
jmp_if_true done
push parameter1
push parameter2
call start_subroutine
done:ret

It's a lot more complex and you get at least 3 jumps (1 test to see if were done, one call and one return).
Also in recursion the parameters need to be set up and fetched.
None of this stuff is needed in a loop because all the parameters are set up already.

Theoretically the parameters could stay in place with recursion as well, but no compilers that I know of actually go that far in their optimization.

Differences between a call and a jmp
A call-return pair is not very much more expensive then the jmp. The pair takes 2 cycles and the jmp takes 1; hardly noticeable.
In calling conventions that support register parameters the overhead for parameters in minimal, but even stack parameters are cheap as long as the CPU's buffers do not overflow.
It is the overhead of the call setup dictated by the calling convention and parameter handling in use that slows down recursion.
This is very much implementation dependent.

Example of poor recursion handling For example, if a parameter is passed that is reference counted (e.g. a non const managed type parameter) it will add a 100 cycles doing a locked adjustment of the reference count, totally killing performance vs a loop.
In languages that are tuned to recursion this bad behavior does not occur.

CPU optimization
The other reason recursion is slower is that it works against the optimization mechanisms in CPU's.
Returns can only be predicted correctly if there are not too many of them in a row. The CPU has a return stack buffer with a (few) handfuls of entries. Once those run out every additional return will be mispredicted causing huge delays.
On any CPU that uses a stack return buffer call based recursion that exceeds the buffer size is best avoided.

About trivial code examples using recursion
If you use a trivial example of recursion like Fibonacci number generation, then these effects do not occur, because any compiler that is 'knows' about recursion will transform it into a loop, just like any programmer worth his salt would.
If you run these trivial examples in a enviroment that does not optimize properly than the call stack will (needlessly) grow out of bounds.

About tail recursion
Note that sometimes the compiler optimizes away tail recursion by changing it into a loop. It is best to only rely on this behavior in languages that have a known good track record in this regard.
Many languages insert hidden clean up code before the final return preventing the optimization of tail recursion.

Confusion between true and pseudo recursion
If your programming environment turns your recursive source code into a loop, then it is arguably not true recursion that is being executed.
True recursion requires a store of breadcrumbs, so that the recursive routine can trace back its steps after it exits.
It is the handling of this trail that makes recursion slower than using a loop. This effect is magnified by current CPU implementations as outlined above.

Effect of the programming environment
If your language is tuned towards recursion optimization then by all means go ahead and use recursion at every opportunity. In most cases the language will turn your recursion into some sort of loop.
In those cases where it cannot, the programmer would be hard pressed as well. If your programming language is not tuned towards recursion, then it should be avoided unless the domain is suited towards recursion.
Unfortunately many languages do not handle recursion well.

Misuse of recursion
There is no need to calculate the Fibonacci sequence using recursion, in fact it is a pathological example.
Recursion is best used in languages that explicitly support it or in domains where recursion shines, like the handling of data stored in a tree.

I understand any recursion can be written as a loop

Yes, if you are willing to put the cart before the horse.
All instances of recursion can be written as a loop, some of those instances require you to use an explicit stack like storage.
If you need to roll your own stack just to turn recursive code into a loop you might as well use plain recursion.
Unless of course you've got special needs like using enumerators in a tree structure and you don't have proper language support.

Answer from Johan on Stack Exchange
Top answer
1 of 3
19

The reason that loops are faster than recursion is easy.
A loop looks like this in assembly.

mov loopcounter,i
dowork:/do work
dec loopcounter
jmp_if_not_zero dowork

A single conditional jump and some bookkeeping for the loop counter.

Recursion (when it isn't or cannot be optimized by the compiler) looks like this:

start_subroutine:
pop parameter1
pop parameter2
dowork://dowork
test something
jmp_if_true done
push parameter1
push parameter2
call start_subroutine
done:ret

It's a lot more complex and you get at least 3 jumps (1 test to see if were done, one call and one return).
Also in recursion the parameters need to be set up and fetched.
None of this stuff is needed in a loop because all the parameters are set up already.

Theoretically the parameters could stay in place with recursion as well, but no compilers that I know of actually go that far in their optimization.

Differences between a call and a jmp
A call-return pair is not very much more expensive then the jmp. The pair takes 2 cycles and the jmp takes 1; hardly noticeable.
In calling conventions that support register parameters the overhead for parameters in minimal, but even stack parameters are cheap as long as the CPU's buffers do not overflow.
It is the overhead of the call setup dictated by the calling convention and parameter handling in use that slows down recursion.
This is very much implementation dependent.

Example of poor recursion handling For example, if a parameter is passed that is reference counted (e.g. a non const managed type parameter) it will add a 100 cycles doing a locked adjustment of the reference count, totally killing performance vs a loop.
In languages that are tuned to recursion this bad behavior does not occur.

CPU optimization
The other reason recursion is slower is that it works against the optimization mechanisms in CPU's.
Returns can only be predicted correctly if there are not too many of them in a row. The CPU has a return stack buffer with a (few) handfuls of entries. Once those run out every additional return will be mispredicted causing huge delays.
On any CPU that uses a stack return buffer call based recursion that exceeds the buffer size is best avoided.

About trivial code examples using recursion
If you use a trivial example of recursion like Fibonacci number generation, then these effects do not occur, because any compiler that is 'knows' about recursion will transform it into a loop, just like any programmer worth his salt would.
If you run these trivial examples in a enviroment that does not optimize properly than the call stack will (needlessly) grow out of bounds.

About tail recursion
Note that sometimes the compiler optimizes away tail recursion by changing it into a loop. It is best to only rely on this behavior in languages that have a known good track record in this regard.
Many languages insert hidden clean up code before the final return preventing the optimization of tail recursion.

Confusion between true and pseudo recursion
If your programming environment turns your recursive source code into a loop, then it is arguably not true recursion that is being executed.
True recursion requires a store of breadcrumbs, so that the recursive routine can trace back its steps after it exits.
It is the handling of this trail that makes recursion slower than using a loop. This effect is magnified by current CPU implementations as outlined above.

Effect of the programming environment
If your language is tuned towards recursion optimization then by all means go ahead and use recursion at every opportunity. In most cases the language will turn your recursion into some sort of loop.
In those cases where it cannot, the programmer would be hard pressed as well. If your programming language is not tuned towards recursion, then it should be avoided unless the domain is suited towards recursion.
Unfortunately many languages do not handle recursion well.

Misuse of recursion
There is no need to calculate the Fibonacci sequence using recursion, in fact it is a pathological example.
Recursion is best used in languages that explicitly support it or in domains where recursion shines, like the handling of data stored in a tree.

I understand any recursion can be written as a loop

Yes, if you are willing to put the cart before the horse.
All instances of recursion can be written as a loop, some of those instances require you to use an explicit stack like storage.
If you need to roll your own stack just to turn recursive code into a loop you might as well use plain recursion.
Unless of course you've got special needs like using enumerators in a tree structure and you don't have proper language support.

2 of 3
17

These other answers are somewhat misleading. I agree that they state implementation details that can explain this disparity, but they overstate the case. As correctly suggested by jmite, they are implementation-oriented toward broken implementations of function calls/recursion. Many languages implement loops via recursion, so loops are clearly not going to be faster in those languages. Recursion is in no way less efficient than looping (when both are applicable) in theory. Let me quote the abstract to Guy Steele's 1977 paper Debunking the "Expensive Procedure Call" Myth or, Procedure Implementations Considered Harmful or, Lambda: the Ultimate GOTO

Folklore states that GOTO statements are "cheap", while procedure calls are "expensive". This myth is largely a result of poorly designed language implementations. The historical growth of this myth is considered. Both theoretical ideas and an existing implementation are discussed which debunk this myth. It is shown that the unrestricted use of procedure calls permits great stylistic freedom. In particular, any flowchart can be written as a "structured" program without introducing extra variables. The difficulty with the GOTO statement and the procedure call is characterized as a conflict between abstract programming concepts and concrete language constructs.

The "conflict between abstract programming concepts and concrete language constructs" can be seen from the fact that most theoretical models of, for example, the untyped lambda calculus, don't have a stack. Of course, this conflict is not necessary as the above paper illustrates and as is also demonstrated by languages who have no iteration mechanism other than recursion such as Haskell.

Let me demonstrate. For simplicity, I'll use an "applied" lambda calculus with numbers and and booleans, and I'll assume we have a fixed-point combinator, fix, which satisfies fix f x = f (fix f) x. All of this can be reduced to just the untyped lambda calculus without changing my argument. The archetypal way of understanding the evaluation of the lambda calculus is through term rewriting with the central rewrite rule of beta reduction, namely where means "replace all free occurrences of in with " and means "rewrites to". This is just the formalization of substituting the arguments of a function call into the functions body.

Now for an example. Define fact as

fact = fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1

Here's the evaluation of fact 3, where, for compactness, I'll use g as synonym for fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)), i.e. fact = g 1. This doesn't affect my argument.

fact 3 
~> g 1 3
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 1 3 
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 1 3
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 1 3
~> (λn.if n == 0 then 1 else g (1*n) (n-1)) 3
~> if 3 == 0 then 1 else g (1*3) (3-1)
~> g (1*3) (3-1)
~> g 3 2
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 3 2
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 3 2
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 3 2
~> (λn.if n == 0 then 3 else g (3*n) (n-1)) 2
~> if 2 == 0 then 3 else g (3*2) (2-1)
~> g (3*2) (2-1)
~> g 6 1
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 1
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 1
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 1
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 1
~> if 1 == 0 then 6 else g (6*1) (1-1)
~> g (6*1) (1-1)
~> g 6 0
~> fix (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) 6 0
~> (λf.λa.λn.if n == 0 then a else f (a*n) (n-1)) g 6 0
~> (λa.λn.if n == 0 then a else g (a*n) (n-1)) 6 0
~> (λn.if n == 0 then 6 else g (6*n) (n-1)) 0
~> if 0 == 0 then 6 else g (6*0) (0-1)
~> 6

You can see from the shape without even looking at the details that there is no growth and each iteration needs the same amount of space. (Technically, the numeric result grows which is unavoidable and just as true for a while loop.) I defy you to point out the boundlessly growing "stack" here.

It seems the archetypal semantics of the lambda calculus already does what is commonly misnamed "tail call optimization". Of course, no "optimization" is happening here. There are no special rules here for "tail" calls as opposed to "normal" calls. For this reason, it's hard to give an "abstract" characterization of what tail call "optimization" is doing, as in many abstract characterizations of function call semantics, there is nothing for tail call "optimization" to do!

That the analogous definition of fact in many languages "stack overflows", is a failure by those languages to correctly implement function call semantics. (Some languages have an excuse.) The situation is roughly analogous to having a language implementation that implemented arrays with linked lists. Indexing into such "arrays" would then be an O(n) operation which doesn't meet the expectation of arrays. If I made a separate implementation of the language, that used real arrays instead of linked lists, you wouldn't say I've implemented "array access optimization", you would say I fixed a broken implementation of arrays.

So, responding to Veedrac's answer. Stacks are not "fundamental" to recursion. To the extent that "stack-like" behavior occurs during the course of evaluation, this can only happen in cases where loops (without an auxiliary data structure) would not be applicable in the first place! To put it another way, I can implement loops with recursion with exactly the same performance characteristics. Indeed, Scheme and SML both contain looping constructs, but both of them define those in terms of recursion (and, at least in Scheme, do is often implemented as a macro that expands into recursive calls.) Similarly, for Johan's answer, nothing says a compiler must emit the assembly Johan described for recursion. Indeed, nothing says a compiler can't emit exactly the same assembly whether you use loops or recursion. The only time the compiler would be (somewhat) obligated to emit assembly like what Johan describes is when you are doing something that isn't expressible by a loop anyway. As outlined in Steele's paper and demonstrated by the actual practice of languages like Haskell, Scheme, and SML, it is not "exceedingly rare" that tail calls can be "optimized", they can always be "optimized". Whether a particular use of recursion will run in constant space depends on how it is written, but the restrictions you need to apply to make that possible are the restrictions you'd need to fit your problem into the shape of a loop. (Actually, they are less stringent. There are problems, such as encoding state machines, that are more cleanly and efficiently handled via tails calls as opposed to loops which would require auxiliary variables.) Again, the only time recursion requires doing more work is when your code isn't a loop anyway.

My guess is Johan is referring to C compilers which have arbitrary restrictions on when it will perform tail call "optimization". Johan also is presumably referring to languages like C++ and Rust when he talks about "languages with managed types". The RAII idiom from C++ and present in Rust as well makes things which superficially look like tail calls, not tail calls (because the "destructors" still need to be called). There have been proposals to use a different syntax to opt-in to a slightly different semantics that would allow tail recursion (namely call destructors before the final tail call and obviously disallow accessing "destroyed" objects). (Garbage collection has no such issue, and all of Haskell, SML, and Scheme are garbage collected languages.) In a quite different vein, some languages, such as Smalltalk, expose the "stack" as a first-class object, in these cases the "stack" is no longer an implementation detail, though this doesn't preclude having separate types of calls with different semantics. (Java says it can't due to the way it handles some aspects of security, but this is actually false.)

In practice, the prevalence of broken implementations of function calls come from three main factors. First, many languages inherit the broken implementation from their implementation language (usually C). Second, deterministic resource management is nice and does make the issue more complicated, though only a handful of languages offer this. Third, and, in my experience, the reason most people care about, is that they want stack traces when errors occur for debugging purposes. Only the second reason is one that can be potentially theoretically motivated.

Top answer
1 of 15
461

This depends on the language being used. You wrote 'language-agnostic', so I'll give some examples.

In Java, C, and Python, recursion is fairly expensive compared to iteration (in general) because it requires the allocation of a new stack frame. In some C compilers, one can use a compiler flag to eliminate this overhead, which transforms certain types of recursion (actually, certain types of tail calls) into jumps instead of function calls.

In functional programming language implementations, sometimes, iteration can be very expensive and recursion can be very cheap. In many, recursion is transformed into a simple jump, but changing the loop variable (which is mutable) sometimes requires some relatively heavy operations, especially on implementations which support multiple threads of execution. Mutation is expensive in some of these environments because of the interaction between the mutator and the garbage collector, if both might be running at the same time.

I know that in some Scheme implementations, recursion will generally be faster than looping.

In short, the answer depends on the code and the implementation. Use whatever style you prefer. If you're using a functional language, recursion might be faster. If you're using an imperative language, iteration is probably faster. In some environments, both methods will result in the same assembly being generated (put that in your pipe and smoke it).

Addendum: In some environments, the best alternative is neither recursion nor iteration but instead higher order functions. These include "map", "filter", and "reduce" (which is also called "fold"). Not only are these the preferred style, not only are they often cleaner, but in some environments these functions are the first (or only) to get a boost from automatic parallelization — so they can be significantly faster than either iteration or recursion. Data Parallel Haskell is an example of such an environment.

List comprehensions are another alternative, but these are usually just syntactic sugar for iteration, recursion, or higher order functions.

2 of 15
88

is recursion ever faster than a loop?

No, Iteration will always be faster than Recursion. (in a Von Neumann Architecture)

Explanation:

If you build the minimum operations of a generic computer from scratch, "Iteration" comes first as a building block and is less resource intensive than "recursion", ergo is faster.

Building a pseudo-computing-machine from scratch:

Question yourself: What do you need to compute a value, i.e. to follow an algorithm and reach a result?

We will establish a hierarchy of concepts, starting from scratch and defining in first place the basic, core concepts, then build second level concepts with those, and so on.

  1. First Concept: Memory cells, storage, State. To do something you need places to store final and intermediate result values. Let’s assume we have an infinite array of "integer" cells, called Memory, M[0..Infinite].

  2. Instructions: do something - transform a cell, change its value. alter state. Every interesting instruction performs a transformation. Basic instructions are:

    a) Set & move memory cells

    • store a value into memory, e.g.: store 5 m[4]
    • copy a value to another position: e.g.: store m[4] m[8]

    b) Logic and arithmetic

    • and, or, xor, not
    • add, sub, mul, div. e.g. add m[7] m[8]
  3. An Executing Agent: a core in a modern CPU. An "agent" is something that can execute instructions. An Agent can also be a person following the algorithm on paper.

  4. Order of steps: a sequence of instructions: i.e.: do this first, do this after, etc. An imperative sequence of instructions. Even one line expressions are "an imperative sequence of instructions". If you have an expression with a specific "order of evaluation" then you have steps. It means than even a single composed expression has implicit “steps” and also has an implicit local variable (let’s call it “result”). e.g.:

    4 + 3 * 2 - 5
    (- (+ (* 3 2) 4 ) 5)
    (sub (add (mul 3 2) 4 ) 5)  
    

    The expression above implies 3 steps with an implicit "result" variable.

    // pseudocode
    
           1. result = (mul 3 2)
           2. result = (add 4 result)
           3. result = (sub result 5)
    

    So even infix expressions, since you have a specific order of evaluation, are an imperative sequence of instructions. The expression implies a sequence of operations to be made in a specific order, and because there are steps, there is also an implicit "result" intermediate variable.

  5. Instruction Pointer: If you have a sequence of steps, you have also an implicit "instruction pointer". The instruction pointer marks the next instruction, and advances after the instruction is read but before the instruction is executed.

    In this pseudo-computing-machine, the Instruction Pointer is part of Memory. (Note: Normally the Instruction Pointer will be a “special register” in a CPU core, but here we will simplify the concepts and assume all data (registers included) are part of “Memory”)

  6. Jump - Once you have an ordered number of steps and an Instruction Pointer, you can apply the "store" instruction to alter the value of the Instruction Pointer itself. We will call this specific use of the store instruction with a new name: Jump. We use a new name because is easier to think about it as a new concept. By altering the instruction pointer we're instructing the agent to “go to step x“.

  7. Infinite Iteration: By jumping back, now you can make the agent "repeat" a certain number of steps. At this point we have infinite Iteration.

                       1. mov 1000 m[30]
                       2. sub m[30] 1
                       3. jmp-to 2  // infinite loop
    
  8. Conditional - Conditional execution of instructions. With the "conditional" clause, you can conditionally execute one of several instructions based on the current state (which can be set with a previous instruction).

  9. Proper Iteration: Now with the conditional clause, we can escape the infinite loop of the jump back instruction. We have now a conditional loop and then proper Iteration

    1. mov 1000 m[30]
    2. sub m[30] 1
    3. (if not-zero) jump 2  // jump only if the previous 
                            // sub instruction did not result in 0
    
    // this loop will be repeated 1000 times
    // here we have proper ***iteration***, a conditional loop.
    
  10. Naming: giving names to a specific memory location holding data or holding a step. This is just a "convenience" to have. We do not add any new instructions by having the capacity to define “names” for memory locations. “Naming” is not a instruction for the agent, it’s just a convenience to us. Naming makes code (at this point) easier to read and easier to change.

       #define counter m[30]   // name a memory location
       mov 1000 counter
    loop:                      // name a instruction pointer location
        sub counter 1
        (if not-zero) jmp-to loop  
    
  11. One-level subroutine: Suppose there’s a series of steps you need to execute frequently. You can store the steps in a named position in memory and then jump to that position when you need to execute them (call). At the end of the sequence you'll need to return to the point of calling to continue execution. With this mechanism, you’re creating new instructions (subroutines) by composing core instructions.

    Implementation: (no new concepts required)

    • Store the current Instruction Pointer in a predefined memory position
    • jump to the subroutine
    • at the end of the subroutine, you retrieve the Instruction Pointer from the predefined memory location, effectively jumping back to the following instruction of the original call

    Problem with the one-level implementation: You cannot call another subroutine from a subroutine. If you do, you'll overwrite the returning address (global variable), so you cannot nest calls.

    To have a better Implementation for subroutines: You need a STACK

  12. Stack: You define a memory space to work as a "stack", you can “push” values on the stack, and also “pop” the last “pushed” value. To implement a stack you'll need a Stack Pointer (similar to the Instruction Pointer) which points to the actual “head” of the stack. When you “push” a value, the stack pointer decrements and you store the value. When you “pop”, you get the value at the actual Stack Pointer and then the Stack Pointer is incremented.

  13. Subroutines Now that we have a stack we can implement proper subroutines allowing nested calls. The implementation is similar, but instead of storing the Instruction Pointer in a predefined memory position, we "push" the value of the IP in the stack. At the end of the subroutine, we just “pop” the value from the stack, effectively jumping back to the instruction after the original call. This implementation, having a “stack” allows calling a subroutine from another subroutine. With this implementation we can create several levels of abstraction when defining new instructions as subroutines, by using core instructions or other subroutines as building blocks.

  14. Recursion: What happens when a subroutine calls itself?. This is called "recursion".

    Problem: Overwriting the local intermediate results a subroutine can be storing in memory. Since you are calling/reusing the same steps, if the intermediate result are stored in predefined memory locations (global variables) they will be overwritten on the nested calls.

    Solution: To allow recursion, subroutines should store local intermediate results in the stack, therefore, on each recursive call (direct or indirect) the intermediate results are stored in different memory locations.

...

having reached recursion we stop here.

Conclusion:

In a Von Neumann Architecture, clearly "Iteration" is a simpler/basic concept than “Recursion". We have a form of "Iteration" at level 7, while "Recursion" is at level 14 of the concepts hierarchy.

Iteration will always be faster in machine code because it implies less instructions therefore less CPU cycles.

Which one is "better"?

  • You should use "iteration" when you are processing simple, sequential data structures, and everywhere a “simple loop” will do.

  • You should use "recursion" when you need to process a recursive data structure (I like to call them “Fractal Data Structures”), or when the recursive solution is clearly more “elegant”.

Advice: use the best tool for the job, but understand the inner workings of each tool in order to choose wisely.

Finally, note that you have plenty of opportunities to use recursion. You have Recursive Data Structures everywhere, you’re looking at one now: parts of the DOM supporting what you are reading are a RDS, a JSON expression is a RDS, the hierarchical file system in your computer is a RDS, i.e: you have a root directory, containing files and directories, every directory containing files and directories, every one of those directories containing files and directories...

Top answer
1 of 5
59

I don't even have to read your code.

Loop is more efficient for factorials. When you do recursion, you have up to x function calls on the stack.

You almost never use recursion for performance reasons. You use recursion to make the problem more simple.

2 of 5
17

Mu.

Seriously now, it doesn't matter. Not for examples this size. They both have the same complexity. If your code is not fast enough for you, this is probably one of the last places you'd look at.

Now, if you really want to know which is faster, measure them. On SBCL, you can call each function in a loop and measure the time. Since you have two simple functions, time is enough. If your program was more complicated, a profiler would be more useful. Hint: if you don't need a profiler for your measurements, you probably don't need to worry about performance.

On my machine (SBCL 64 bit), I ran your functions and got this:

CL-USER> (time (loop repeat 1000 do (factorial_recursion 1000)))
Evaluation took:
  0.540 seconds of real time
  0.536034 seconds of total run time (0.496031 user, 0.040003 system)
  [ Run times consist of 0.096 seconds GC time, and 0.441 seconds non-GC time. ]
  99.26% CPU
  1,006,632,438 processor cycles
  511,315,904 bytes consed

NIL
CL-USER> (time (loop repeat 1000 do (factorial_loop 1000)))
Evaluation took:
  0.485 seconds of real time
  0.488030 seconds of total run time (0.488030 user, 0.000000 system)
  [ Run times consist of 0.072 seconds GC time, and 0.417 seconds non-GC time. ]
  100.62% CPU
  902,043,247 processor cycles
  511,322,400 bytes consed

NIL

After putting your functions in a file with (declaim (optimize speed)) at the top, the recursion time dropped to 504 milliseconds and the loop time dropped to 475 milliseconds.

And if you really want to know what's going on, try dissasemble on your functions and see what's in there.

Again, this looks like a non-issue to me. Personally, I try to use Common Lisp like a scripting language for prototyping, then profile and optimize the parts that are slow. Getting from 500ms to 475ms is nothing. For instance, in some personal code, I got a couple of orders of magnitude speedup by simply adding an element type to an array (thus making the array storage 64 times smaller in my case). Sure, in theory it would have been faster to reuse that array (after making it smaller) and not allocate it over and over. But simply adding :element-type bit to it was enough for my situation - more changes would have required more time for very little extra benefit. Maybe I'm sloppy, but 'fast' and 'slow' don't mean much to me. I prefer 'fast enough' and 'too slow'. Both your functions are 'fast enough' in most cases (or both are 'too slow' in some cases) so there's no real difference between them.

🌐
Reddit
reddit.com › r/c_programming › is recursive function faster than for loops?
r/C_Programming on Reddit: Is recursive function faster than for loops?
February 12, 2024 -

Is it faster to use a recursive function and define a stop condition in it other than using for loop or while? Ive saw some functional languages that dont have a loop and use recursive functions and also remembered in the past creating a factorial function that is recursive, a function to convert from decimal to binary and so on…

🌐
Medium
medium.com › nerd-for-tech › practically-understanding-time-complexities-of-recursive-and-iterative-functions-95238525c145
Practically understanding time complexities of Recursive and Iterative functions
May 30, 2021 - ... As it is observed in the algorithm, Recursive function keeps calling itself till a base condition (i.e n<2) is reached. While the iterative function uses for loop to repeatedly execute a set of instructions (in this case: z=x+y; x=y; y=z;).
🌐
Reddit
reddit.com › r/learnpython › loops vs recursion
r/learnpython on Reddit: Loops vs recursion
April 30, 2018 -

So I was watching this video on youtube and was simply amazed by this function to calculate factorials with recursions.

def factorials(n):
    if n == 1:
        return 1
    else:
        return n * factorial(n-1)

Since I'm pretty new to python and coding in general (I'm a lawyer by profession), I would have solved this problem with loops. The moment I understood the concept of this function, I was baffled by the simplicity and beauty of it.

Now here's my question:

  1. In which cases is it smarter to use one or the other and what are the benefits? (if there even is a general rule)

  2. Where can I learn more about recursions? Are there any tutorials online?

Thanks :)

Top answer
1 of 5
36
Every recursive solution can be expressed as a loop, and vice versa. In Python, you use recursion when the recursive solution makes it easier to understand (you would probably use a loop for a factorial) Recursions are heavily used in functional programming (which Python does not excel at), and Guido said he prefers loops instead of recursion. If you like recursion and mathematical elegance in programming, I recommend you take a look at functional programming and functional programming languages. Be advised though that functional programming can be difficult to grasp at first.
2 of 5
20
You CAN solve almost any problem using almost any loop. But when is it more appropriate to use which loop, my opinion is this: A while loop is the correct solution when the data amount and endpoint is unknown. A for loop is the correct solution when the amount of data, and endpoint is known. A Recursion is the correct solution when the amount is known, but the end point is not. So what I mean by that is, imagine finding a file in a bunch of folders. Inside every folder the AMOUNT of files are known, so you use a for loop to walk through the files and you pick out the right file. But how many folders do you need to go through? You don't know that, because every subfolder COULD have another subfolder, or not. And so the folder structure branches out, in an unknown way. This becomes difficult to deal with using multiple for loops. Again, because the endpoint is unknown. Here recursion is the perfect solution. find_file(folder): global filelist for file in folder.files: if ".jpg" in file.name: filelist.append(file) for subfolder in folder.dirs: find_file(subfolder)
🌐
Quora
quora.com › Which-one-is-better-loop-or-recursion-in-terms-of-time-and-space
Which one is better, loop or recursion in terms of time and space? - Quora
Answer: Using a loop construct where applicable is normally more efficient in time and space. However, many modern languages/compilers perform tail call optimisation, which means that there maybe no difference between loops and recursive tail calls. For more complex forms of nesting there is no b...
🌐
Medium
varshacbendre.medium.com › is-recursion-faster-than-looping-18ce596c51e6
Recursive Adventures: Unleashing the Power of Recursion vs. Looping | by Varsha C Bendre | Medium
April 6, 2025 - When the low > high condition is encountered in the while loop, it exits before the beginning of the next while loop and returns False. ... The time complexity is O(log n) which is the same for both recursive and iterative binary searches.
🌐
Appsmith
community.appsmith.com › content › blog › recursion-vs-loops-simple-introduction-elegant-javascript
Recursion Vs. Loops: A Simple Introduction to Elegant Javascript | Appsmith Community Portal
July 12, 2023 - Performance Concerns: Loops are generally more efficient than recursion regarding time and space complexity. Recursive calls can lead to increased memory usage because each function call is added to the stack, while a loop only requires a single ...
Find elsewhere
🌐
Enjoy Algorithms
enjoyalgorithms.com › blog › difference-between-iteration-and-recursion
Recursion vs Iteration Comparison
Both iteration and recursion can be prone to small but critical mistakes, such as the "off by one" error in iteration or passing the wrong value to function parameters in recursion. It is important to carefully consider and test conditions and variables involved in these approaches to avoid such errors. In general, the analysis of iterative code is relatively simple as it involves counting the number of loop iterations and multiplying that by the time complexity of the code executed at each iteration.
🌐
DEV Community
dev.to › sandrockjustin › recursion-or-loop-3og1
Recursion or Loop? - DEV Community
August 9, 2024 - A recursive approach can be great for some problems, such as working within lists or sequences. However, a recursive approach has a greater deal of code complexity than a looping ...
🌐
Medium
medium.com › @ccfcheng › algorithmic-runtime-recursion-vs-iteration-b8b42bf6dfe3
Algorithmic Runtime: Recursion vs. Iteration | by Cliff Saporta Cheng | Medium
November 4, 2016 - In a nutshell, we loop in recursion by having a function make a call to a new version of itself during execution, and in iteration, we loop by having a function repeat an action over and over until we hit some specified ending condition. In theory, any function that can be written recursively can also be written iteratively, and vice versa. In practice, we may make a specific choice based on whether we value runtime complexity or readability, and based on the nature of the problem itself.
🌐
Baeldung
baeldung.com › home › core concepts › recursion and looping
Recursion and Looping | Baeldung on Computer Science
March 18, 2024 - The function fact() runs a loop 5 times and finally returns the factorial of 5 to the function main(). Further, when we run this code for 100000 iterations, then we find its execution time as 3.86 microseconds. Please note that this time would vary from machine to machine. In this section, we’ll go through the concept of recursion.
🌐
PREP INSTA
prepinsta.com › home › data structures and algorithms in python › time complexity of recursive loops
Time Complexity of recursive loops | PrepInsta
August 1, 2025 - The time complexity depends on how many recursive calls are made and how much work is done per call. Recursive loops involve a function that calls itself directly or indirectly in a self-referential manner.
Top answer
1 of 3
1

Advice for optimization.

Avoid function calls. Avoid creating temporary garbage. Avoid extra comparisons. Have logic that looks at elements as little as possible. Walk through how your code works by hand and look at how many steps it takes.

Your recursive code makes only 3 function calls, and as pointed out elsewhere does an average of 1.5 comparisons per call. (1 while looking for the min, 0.5 while figuring out where to remove the element.)

Your iterative code makes lots of comparisons per element, calls excess functions, and makes calls to things like sorted that create/destroy junk.

Now compare with this iterative solution:

def find_largest(array, limit=3):
    if len(array) <= limit:
        # Special logic not needed.
        return sorted(array)
    else:
        # Initialize the answer to values that will be replaced.
        min_val = min(array[0:limit])
        answer = [min_val for _ in range(limit)]

        # Now scan for smallest.
        for i in array:
            if answer[0] < i:
                # Sift elements down until we find the right spot.
                j = 1
                while j < limit and answer[j] < i:
                    answer[j-1] = answer[j]
                    j = j+1
                # Now insert.
                answer[j-1] = i

        return answer

There are no function calls. It is possible that you can make up to 6 comparisons per element (verify that answer[0] < i, verify that (j=1) < 3, verify that answer[1] < i, verify that (j=2) < 3, verify that answer[2] < i, then find that (j=3) < 3 is not true). You will hit that worst case if array is sorted. But most of the time you only do the first comparison then move to the next element. No muss, no fuss.

How does it benchmark?

Note that if you wanted the smallest 100 elements, then you'd find it worthwhile to use a smarter data structure such as a heap to avoid the bubble sort.

2 of 3
1

I am not really confortable with python, but I have a different approach to the problem for what it's worth. As far as I saw, all solutions posted are O(NM) where N is the length of the array and M the length of the largest elements array. Because of your specific situation whereN >> M you could say it's O(N), but the longest the inputs the more it will be O(NM) I agree with @zvone that it seems you have more steps in the iterative solution, which sounds like an valid explanation to your different computing speeds. Back to my proposal, implements binary search O(N*logM) with recursion:

import math

def binarySearch(arr, target, origin = 0):
    """ 
    Recursive binary search
    Args:
        arr (list): List of numbers to search in
        target (int): Number to search with
    Returns:
        int: index + 1 from inmmediate lower element to target in arr or -1 if already present or lower than the lowest in arr
    """
    half = math.floor((len(arr) - 1) / 2);
    if target > arr[-1]:
        return origin + len(arr)
    if len(arr) == 1 or target < arr[0]:
        return -1
    if arr[half] < target and arr[half+1] > target:
        return origin + half + 1
    if arr[half] == target or arr[half+1] == target:
        return -1
    if arr[half] < target:
        return binarySearch(arr[half:], target, origin + half)
    if arr[half] > target: 
        return binarySearch(arr[:half + 1], target, origin)
    
def findLargestNumbers(array, limit = 3, result = []):
    """ 
    Recursive linear search of the largest values in an array
    Args:
        array (list): Array of numbers to search in
        limit (int): Length of array returned. Default: 3
    Returns:
        list: Array of max values with length as limit
    """    
    if len(result) == 0:
        result = [float('-inf')] * limit
    if len(array) < 1:
        return result
    val = array[-1]
    foundIndex = binarySearch(result, val)
    if foundIndex != -1:
        result.insert(foundIndex, val)
        return findLargestNumbers(array[:-1],limit, result[1:])
    return findLargestNumbers(array[:-1], limit,result)

It is quite flexible and might be inspiration for a more elaborated answer.

🌐
Slash
slash.co › home › articles › 5 brief explanation about the difference between recursion vs iteration
5 Brief explanation about the difference between recursion vs iteration - Slash
February 21, 2024 - As the recursion grows, the call stack expands, potentially using significant memory. Besides, this cycled function calling increases the code’s time complexity, making it feeble or less efficient. However, iterative functions avoid using overheads. Instead, this approach uses loop control structures to control flow, manage variables, and repeat code blocks.
🌐
JavaScript in Plain English
javascript.plainenglish.io › recursive-algorithms-and-their-time-complexities-o-n-vs-2-n-713856ad4e2
Recursive Algorithms and Their Time Complexities O(n) vs O(2^n) | by Code Ceeker | JavaScript in Plain English
June 13, 2023 - So what would be the time complexity of this recursive function? It’s not O(n) definitely, which was the case for the loop-based solution. We got (9) executions for 4, (15) executions for 5, and (25) executions for 6. So if we increase the number that we feed to the function by just 1, we have an exponential increase in executions.