Content inspiration: Thoughts on midtail recursion in Swift [4] — Thuyen’s Corner.

Like the comment and hope it gets better and better with your help

Author: @ios growth refers to north, this article was first published in the public number iOS growth refers to north, welcome to the correction

Please contact me if there is a need to reprint, remember to contact oh!!

This is the sixth day of my participation in Gwen Challenge

What is recursion and what is tail recursion \ call

Recursive invocation is a common technique used in the development process to implement calls to functions themselves.

People often use recursion for two reasons:

  • Make code shorter and more concise
  • Node traversal of high-level structures (trees, graphs)

When we call the function itself at the end of a function, it is called tail recursion, and when we call other functions, it is called tail call [1].

Our function call will form a call record in memory, also called call frame [2], which stores the information of the call location and internal variables. These call frames form the Call stack during nested calls.

Tail Call optimization (TCO), which is used in most languages, keeps only the call record of the inner function. If all functions are called by Tail, then it is possible to make only one call record per execution. This will save a lot of memory. This is the meaning of tail-call optimization [3].

Recursion is very memory intensive because you need to hold hundreds or thousands of call records at the same time, which is prone to stack overflow errors. For tail recursion, however, stack overflow errors never occur because there is only one call record.

Some functional programming languages write this into the language specification, so does this optimization exist in iOS? What does this optimization do?

We are going to focus on Swift, right? Trust me, this problem is presented similarly in Objective-C.

Tail recursion crashes in Debug mode

Consider the following code: what happens in default Debug mode?

func sumPrefix(_ n: Int._ acc: Int) -> Int {
  if n = = 0 { return acc }
  return sumPrefix(n - 1, acc + n)
}

_ = sumPrefix(1000000.0)
Copy the code

A regular operation that computes the sum from 0 to 1000000.

When we run the current project, eh? collapsed

An EXC_BAD_ACCESS error occurred. This error occurs when a message is sent to freed memory or when an attempt is made to access a memory segment that has been freed — a segmentation fault.

Note that we are talking about the default Debug mode, which means that relevant compilation optimizations are not enabled

But when we add the same compilation optimizations as in Release mode, the code works fine!

Swift tail call optimization

Let’s compare how the same code behaves with tuning turned on.

Converting the code to assembly, what is the phenomenon of the current function call when the configuration item is -onone (some redactions for article)

_main:
	.cfi_startproc
	...
	callq	main.sumPrefix(Swift.Int, Swift.Int) -> Swift.Int
	...
	.cfi_endproc

	.private_extern	main.sumPrefix(Swift.Int, Swift.Int) -> Swift.Int
	.globl	main.sumPrefix(Swift.Int, Swift.Int) -> Swift.Int
	.p2align	4, 0x90
main.sumPrefix(Swift.Int, Swift.Int) -> Swift.Int:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$80, %rsp
	xorl	%eax, %eax
	leaq	-8(%rbp), %rcx
	...
	callq	_memset
	...
	callq	_memset
	...
	cmpq	$0, %rcx
	jne	LBB1_2
	...
	jmp	LBB1_5
LBB1_2:
	...
	callq	main.sumPrefix(Swift.Int, Swift.Int) -> Swift.Int
	movq	%rax, -56(%rbp)
LBB1_5:
	movq	-56(%rbp), %rax
	addq	$80, %rsp
	popq	%rbp
	retq
LBB1_6:
	ud2
LBB1_7:
	ud2
	.cfi_endproc
Copy the code

Here a number of _memset function calls, in the process of running clear memory initialization operation. And when n! = 0 call jne LBB1_2, execute LBB1_2 method, callq main.sumprefix (swift.int, swift.int) -> swift.int.

As we run the function, we keep creating new stack space, which eventually overflows the stack and causes a crash.

Optimize for Speed[O] when we use the -o enabled Release environment :(untrimmed)

_main:
	movl	$1000000, %eax
	xorl	%ecx, %ecx
	.p2align	4, 0x90
	
	.private_extern	main.sumPrefix(Swift.Int, Swift.Int) -> Swift.Int
	.globl	main.sumPrefix(Swift.Int, Swift.Int) -> Swift.Int
	.p2align	4, 0x90
main.sumPrefix(Swift.Int, Swift.Int) -> Swift.Int:
	pushq	%rbp
	movq	%rsp, %rbp
	movq	%rsi, %rax
	testq	%rdi, %rdi
	je	LBB1_5
	movq	%rdi, %rcx
	.p2align	4, 0x90
LBB1_2:
	decq	%rcx
	jo	LBB1_6
	addq	%rdi, %rax
	jo	LBB1_7
	movq	%rcx, %rdi
	testq	%rcx, %rcx
	jne	LBB1_2
LBB1_5:
	popq	%rbp
	retq
LBB1_6:
	## InlineAsm Start
	## InlineAsm End
	ud2
LBB1_7:
	## InlineAsm Start
	## InlineAsm End
	ud2
Copy the code

The comparison found:

  • After starting optimization, there is no callq _memset

  • Callq main.sumPrefix(swift.int, swift.int) -> swift.int

Tail-recursion/call optimization can reuse stack space and reuse stack frames without requesting stack space, increasing efficiency and ensuring security.

Note that we didn’t find the sumPrefix call in the main function when we turned it on, because the -o that is Optimize for Speed[O] is another optimization point that the compiler will Optimize inline.

Inline optimization

Inline optimization, that is, some methods expand it directly into function body code to reduce the application of function stack frames for faster performance.

MJ said in his Swift class that recursive calls do not perform inline optimization. From the assembly code obtained so far, tail-recursive/call optimization is performed inline optimization.

We turn off the inlining optimization of the current function and use @inline(never) to mark the function to never be inlined.

@inline(never)
func sumPrefix(_ n: Int._ acc: Int) -> Int {
  if n = = 0 { return acc }
  return sumPrefix(n - 1, acc + n)
}
Copy the code

Then we -o export the compilation for analysis :(undeleted)

_main:
	pushq	%rbp
	movq	%rsp, %rbp
	movl	$1000000, %edi
	xorl	%esi, %esi
	callq	main.sumPrefix(Swift.Int, Swift.Int) -> Swift.Int
	xorl	%eax, %eax
	popq	%rbp
	retq

	.private_extern	main.sumPrefix(Swift.Int, Swift.Int) -> Swift.Int
	.globl	main.sumPrefix(Swift.Int, Swift.Int) -> Swift.Int
	.p2align	4, 0x90
main.sumPrefix(Swift.Int, Swift.Int) -> Swift.Int:
	pushq	%rbp
	movq	%rsp, %rbp
	movq	%rsi, %rax
	testq	%rdi, %rdi
	je	LBB1_5
	movq	%rdi, %rcx
	.p2align	4, 0x90
LBB1_2:
	decq	%rcx
	jo	LBB1_6
	addq	%rdi, %rax
	jo	LBB1_7
	movq	%rcx, %rdi
	testq	%rcx, %rcx
	jne	LBB1_2
LBB1_5:
	popq	%rbp
	retq
LBB1_6:
	## InlineAsm Start
	## InlineAsm End
	ud2
LBB1_7:
	## InlineAsm Start
	## InlineAsm End
	ud2

	.section	__TEXT,__swift5_entry,regular,no_dead_strip
	.p2align	2
l_entry_point:
Copy the code

Callq main.sumprefix (swift.int, swift.int) -> swift.int, the body of the method call remains unchanged before the inlining is turned off.

Tail recursion/call optimization implementation principle: When the last step of function A is to call its own function A (or call other functions B), the position information and internal variables of the current function A are no longer used, and the stack frame of function A is directly handed over to function A (B).

conclusion

When using recursive/nested calls in Debug mode, once the call is too deep, it is possible to overflow the stack and cause a crash. Because maximum depth is not guaranteed, it can cause your build/debug to crash in some cases.

Our example here refers to tail recursion/invocation, which is probably more in line with common invocation logic, and you need to carefully consider whether your code causes too deep a call and whether using recursion is optimal.

In addition to implementing tail recursion/call optimization, Xcode’s compilation optimization for Swift performs inline optimization for recursive functions.

More and more

For Objective-C, the same crashes occur:

- (NSInteger)sumPrefix:(NSInteger)n acc:(NSInteger)acc {
    if (n == 0) {
        return acc;
    }
    return [self sumPrefix:n - 1 acc: acc + n];
}
Copy the code

You can explore it in a similar way.

The resources

Tail call: zh.wikipedia.org/wiki/ tail call

Call stack: zh.wikipedia.org/wiki/ Call stack

Tail call optimization: www.ruanyifeng.com/blog/2015/0…

Thinking about Swift in tail recursion: trinhngocthuyen. Making. IO/posts/tech /…


If you have any questions, please comment directly, and feel free to express anything wrong with the article. If you wish, you can spread the word by sharing this article.

Thank you for reading this! 🚀