MethodByName Inflates Your Binary Size
“Reflection is never clear”, says a Go proverb. In fact, reflection is doing a lot of magic, so the general advice is to avoid it until the problem at hand requires reflection.
But even if you don't actively use reflection in your code, nor intend to dig into reflection code in an external library your project imports, reflection can bite you. And it does so in a quite unexpected way.
My course Master Go is $60 off until Dec 2nd.
Hop over to AppliedGo.com for more info.
Certain methods of the reflect package disable DCE
The problem, as short and clear as possible:
If code calls one of
reflect.Value.Method(int)
orreflect.Value.Method(int)
,- or
reflect.Value.MethodByName(string)
orreflect.Type.MethodByName(string)
with a non-constant argument,
the compiler is unable to identify and eliminate dead code.
In any of the above cases, any method becomes potentially reachable at runtime. Dead Code Elimination (DCE) gets paralyzed and reverts to marking all exported methods of all reachable types as reachable.
As a consequence, the size of your binary may be larger than necessary.
Beware of these reflect methods creeping into your code
So, before you let your code reflect on itself, reflect on the necessity of using reflection. (In contrast to code, human brains can gain clarity by reflection.)
Unfortunately, deciding to not using these methods is not enough. It is sufficient to import a seemingly innocent package like text/template
that
uses one of these methods under the hood, in order to unintentionally disable DCE.
(Could someone write a linter, please?)
Worry, but don't worry too much
How much does a binary grow when DCE is disabled?
I made a quick and completely unscientific text with a small project I had at hand. I quickly added a few lines that prepare and test a call to Value.MethodByName()
.
- With a constant string as argument, the compiler created a binary of 8.5MB.
- When I changed the argument to a variable, the binary size increased to 10MB.
This is an increase to 118% of the original size, which is quite moderate and should not be a problem in most usage scenarios. (Your mileage may vary.)
Stripping the binary with go build -ldflags "-s -w" .
reduced the binary sizes to 5.9MB when using a static argument and to 7.1MB when using a variable argument. Stripping symbols from the binary can thus mitigate the effect to a certain extent.
This being said, the effect surely varies with the number of methods the compiler can identify as dead code.
Links
- Hat tip to
/u/Flimsy_Complaint490
for raising this topic. - Dead code elimnation happens in steps 4–7 of compilation.
- “all: avoid non-const reflect.MethodByName calls”
text/template
’s use ofMethodByName
- Three ways of dead code detection in Go 1.23.2 (the current version as of this writing)—Read from the linked line 405 until this comment: “If we find a REFLECTMETHOD, we give up on static analysis, and mark all exported methods of all reachable types as reachable.”
- Not directly related to the problem but still interesting: Finding unreachable functions with deadcode