Using try catch in recursion

just start with code:

-module(test).

-export([run/1, run_1/1]).

run(N) ->
	Self = self(),
	First = timer:tc(fun run_1/1, [N]),
	Second = timer:tc(fun run_1/1, [N]),
	erlang:garbage_collect(),
	spawn(fun() -> Self ! {run_1, timer:tc(fun run_1/1, [N])} end),
	receive
		{run_1, Three} ->
			{First, Second, Three}
	end.

run_1(0)->
	erlang:process_info(self(), memory);
run_1(N) ->
	try
		do_somthing,
		run_1(N - 1)
	catch
		_:_ ->
			error
	end.

runing on erl shell:

Erlang/OTP 25 [erts-13.0] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit:ns]

Eshell V13.0  (abort with ^G)
1> test:run(10000000).
{{2272172,{memory,177426856}},
 {112110,{memory,177426856}},
 {1733515,{memory,177423808}}}
2> test:run(10000000).
{{2255622,{memory,177426856}},
 {103118,{memory,177426856}},
 {1629922,{memory,177423808}}}
3> timer:tc(fun test:run_1/1, [10000000]).
{2251741,{memory,177436632}}
4> timer:tc(fun test:run_1/1, [10000000]).
{90485,{memory,177436632}}
5> timer:tc(fun test:run_1/1, [10000000]).
{130807,{memory,177436632}}

the question is

  • Why it cost so many memory and time on the first run_1/1
  • In spawn seems have a speed up after first execute
  • Continuous execution run_1/1 only slow in the first times

of course, this try...catch recursion just for learning :slightly_smiling_face:

1 Like
  • The code you’re writing here can’t be tail-recursively optimized
1 Like

See this chapter from LYSE, the box just at the end of the chapter.

That said, it is worth noting that your run_1 function nests the protected parts of the try expression again and again, like an onion:

It would be better to move the recursive call out of the protected part and into of ..., like this:

try
    do_something_risky()
of
    _ -> run_1(N-1)
catch
    _:_ -> error
end
3 Likes

@SisMaker @Maria-12648430 thanks for reply!
OK, I get it now :rofl:

1 Like

In fact, we will never write code like this :rofl:

1 Like

Why it cost so many memory and time on the first

First time it reallocates the process heap multiple times, copying very deep stack (because function is not tail-recursive).
Second time the heap is already inflated.
You can verify that adding io:format("~p~n", [element(2, erlang:process_info(self(), total_heap_size))]), before First = timer:tc and after it.

Same reason why second run is faster - the heap does not need to be reallocated (and copied many many times). You can probably verify that printing process_info(Self, garbage_collection) paying attention to how many GCs were done. A better way would be tracing GC with erlang:trace(Pid, true, [garbage_collection]), - it will contain information on how many times GC happened, and how long it took. You can also enable system_monitor to receive large_heap or long_gc events.

Continuous execution, as easily seen, does not expand/shrink the heap, making it relatively fast (except for the first time when heap does expand, and stack + heap do get copied many-many times).

4 Likes

Good :+1: :smile_cat:

1 Like