-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Add missing typespecs and fix error in docs #7259
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add missing typespecs and fix error in docs #7259
Conversation
The module attribute makes no difference in terms of allocations. Every
literal in the code is allocated just once when the code loads, from there
on we have just copies. So we can revert this bit from the commit! :)
--
*José Valimwww.plataformatec.com.br
<http://www.plataformatec.com.br/>Founder and Director of R&D*
|
I tried to create a type for `options` and to move the documentation around which options are available up to a typedoc so that was clear that this is a known type for this module, but it ended up seeming more complicated than it was worth, so I went with the more general type of `keyword` instead. I also did a small optimization of making the default options a module attribute since that keyword list is a constant, so there's no need to allocate a new keyword list every time that function is called.
1a9aa86
to
ef16ed8
Compare
I could have sworn I did some benchmark on something where I extracted a module attribute and it was faster, and I assumed this was because of not having to allocate a copy, but I guess I was wrong or there was something else going on! So, you're saying if you have a reference to a module attribute in the body of a function, that copies the value stored in that module attribute the same way as if it was returned from another function call? So this: @numbers [1, 2, 3]
def count do
Enum.each(@numbers, fn number -> IO.puts number end)
end copies the same way as this: def numbers, do: [1, 2, 3]
def count do
Enum.each(numbers(), fn number -> IO.puts number end)
end |
In both cases there's no copy (since data is immutable). When a module is loaded a special memory area for that module is allocated where all literals are stored. This data lives outside of processes so it's also never traversed during GC. In OTP 20 literals like that are not even copied when sent between processes. The cost is that when module is unloaded, every process heap is traversed to find literals from that module to properly copy them into the process heap so they become normal data like everything else. This can be seen in the assembly for the module: defmodule Test do
@numbers [1, 2, 3]
def count_attribute do
Enum.each(@numbers, fn number -> IO.puts number end)
end
def count_inline do
Enum.each([1, 2, 3], fn number -> IO.puts number end)
end
def numbers, do: [1, 2, 3]
def count_call do
Enum.each(numbers(), fn number -> IO.puts number end)
end
end First the {function, count_attribute, 0, 10}.
{label,9}.
{line,[{location,"test.ex",4}]}.
{func_info,{atom,'Elixir.Test'},{atom,count_attribute},0}.
{label,10}.
{make_fun2,{f,22},0,0,0}.
{move,{x,0},{x,1}}.
{move,{literal,[1,2,3]},{x,0}}.
{line,[{location,"test.ex",5}]}.
{call_ext_only,2,{extfunc,'Elixir.Enum',each,2}}. The The {function, count_inline, 0, 14}.
{label,13}.
{line,[{location,"test.ex",8}]}.
{func_info,{atom,'Elixir.Test'},{atom,count_inline},0}.
{label,14}.
{make_fun2,{f,22},0,0,0}.
{move,{x,0},{x,1}}.
{move,{literal,[1,2,3]},{x,0}}.
{line,[{location,"test.ex",9}]}.
{call_ext_only,2,{extfunc,'Elixir.Enum',each,2}}. Finally, the {function, numbers, 0, 16}.
{label,15}.
{line,[{location,"test.ex",12}]}.
{func_info,{atom,'Elixir.Test'},{atom,numbers},0}.
{label,16}.
{move,{literal,[1,2,3]},{x,0}}.
return.
{function, count_call, 0, 12}.
{label,11}.
{line,[{location,"test.ex",14}]}.
{func_info,{atom,'Elixir.Test'},{atom,count_call},0}.
{label,12}.
{allocate_zero,1,0}.
{line,[{location,"test.ex",15}]}.
{call,0,{f,16}}.
{move,{x,0},{y,0}}.
{make_fun2,{f,24},0,0,0}.
{move,{x,0},{x,1}}.
{move,{y,0},{x,0}}.
{line,[{location,"test.ex",15}]}.
{call_ext_last,2,{extfunc,'Elixir.Enum',each,2},1}. The overhead you're seeing comes from calling the function - there's some additional bookkeeping the VM needs to do (the costliest is allocating the stack frame) and some register shuffling. The data itself, though, is produced exactly the same way with the |
Aha! That makes sense as to why there was a slight slowdown with the function version. Thanks for the wonderful explanation @michalmuskala! |
❤️ 💚 💙 💛 💜 |
I tried to create a type for
options
and to move the documentationaround which options are available up to a typedoc so that was clear
that this is a known type for this module, but it ended up seeming more
complicated than it was worth, so I went with the more general type of
keyword
instead.I also did a small optimization of making the default options a module
attribute since that keyword list is a constant, so there's no need to
allocate a new keyword list every time that function is called.