In your example the value of the argument is a part of the definition. Value of fnc[2 i], where i is a symbol, is not defined in your MWE.
Total@Table[ByteCount[fnc[2 i]], {i, n}]
16 000 000
(Edit: note, that my solution only gives only the size of the right hand side and probably underestimates the real memory cost. See the other solution.)
Note also, that your timing measurement lead to misleading results as you apply it to so simple operations. Compare to
n2 = 10;
AbsoluteTiming[Position[tbl, #] & /@ (2 RandomInteger[n, n2])] // First
AbsoluteTiming[Replace[rls] /@ (2 RandomInteger[n, n2]);] // First
2.19423
1.8028
Both of these approaches are very slow, as they require going through the whole list to fine the element.
These are much much faster:
n2 = 100000;
AbsoluteTiming[fnc /@ (2 RandomInteger[n, n2]);] // First
AbsoluteTiming[asc /@ (2 RandomInteger[n, n2]);] // First
AbsoluteTiming[Lookup[asc, 2 RandomInteger[n, n2]];] // First
> 0.267781
> 0.226777
> 0.171752
I think it is misleading to call fnc[i] a function, as a function usually is evaluated runtime. In your MWE you save a precomputed value. This technique is referred to as memoization.
When you wonder which one you should use, I would always use what makes sense semantically, because the engineers behind the kernel and native commands have but a lot of effort into finding a balance between all the features one usually needs from a List, Rules, Associations and Symbols. Associations and Symbols are the ones, which require fast random access.
foo /@ 2 RandomInteger[n, n2]is parsed as(foo /@ 2) RandomInteger[n, n2]. – Carl Woll Aug 25 '18 at 20:42Listto be that fast. Now it all makes sense. – Johu Aug 25 '18 at 22:46