-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Move Dynlink into the Standard Library and use it in the toplevel #13745
base: trunk
Are you sure you want to change the base?
Conversation
Eliminates the Dynlink_types and Dynlink_platform_intf modules.
Slightly enrich Dynlink.linking_error so that Dynlink_symtable.error becomes unnecessary.
Limited plumbing via hidden sections of Dynlink.
The duplications are now trivial, or at least trivial to maintain.
I'm curious about the stdlib -- given that there's no dead code removal (as far as I know), and the OCaml stdlib is basically ever-growing -- should there be really more and more code in there? Please excuse me if the goal is not to have a small footprint of a hello-world application? Or if this is the wrong PR to ask such a question. In my experience, using a hello world as an example (a single OCaml module using I for one would appreciate to have a plan for link-time removal of code before growing the stdlib further. |
The Standard Library doesn't grow as a result of this PR (module aliases). For your hello world example on my system, the size in bytecode increases by 45 bytes (from 22760 to 22715) - that is probably down to the addition of In native code, there's an increase of 560 bytes, but that's more likely down to the changes in the runtime (in particular the addition of a primitive in callback.n.o) - the assembler produced is identical and stdlib.o doesn't change in size. |
@dra27 thanks for your investigation and comment. |
The original question and subsequent answer is interesting -- but it still leaves some questions unanswered. So if there is no dead code elimination during link time what happens generally to all the unused functions in an OCaml executable-- do they accumulate in the executable ? |
The compiler does not currently perform dead code elimination at the function level. However, it does perform dead code elimination of whole modules: modules of libraries which are completely unused (except perhaps by appearing in module aliases declarations), are removed from the final executable (unless Everything else goes into the final executable. |
Given that OCaml does not jump into the middle of functions, would it not be a not too hard task to simply remove all the functions that are not being used ? A bit surpised that this is not happening already. Is it because of dynamic linking ? The ocaml program might dynamically load a module that might might call arbitrary functions within the executable ? Does dead code elimination happen in flambda ? [I just realized that this discussion is probably off topic by now. Would appreciate any answer. Will take discussion on discuss.ocaml.org subsequently if it feels particularly compelling to continue it] |
You will be interested by #608. |
All functions exposed by a module are stored in the module's static block. So unless you run some kind of escaping analysis on the whole program, everything that is exposed in the signature of a module that is used has to be present in the executable.
In a given module, yes, flambda will remove unused functions. But everything that is exported stays. |
Coming back to this PR, without having gone through the details, it seems like a good simplification from a distance. @dra27 what's the story for backwards comatibility? Are we going to ship a dummy |
One specific aspect of the Dynlink library is that linking with it adds --export-dynamic on the linker's command line (by building dynlink.cmxa with something like Doesn't this change force the dynamic symbol table to always be generated? (That being said, while I see code related to that behavior being deleted, I don't see -Wl,-E in dynlink.cmxa even at the base of the pull request. Not sure if that's an issue with the configure script or what) [1] not to be confused with the symbol table. IIRC, the dynamic symbol table is used by dlsym to lookup symbols (so it's probably a map from symbol to address), whereas the symbol table is used by gdb/perf to display names instead of addresses (so it's probably a map from addresses to symbols). |
@v-gb - I'd wondered this a bit too, and as far as I could tell (certainly looking at 4.14) the flags are equivalent in each case - i.e. |
Indeed, there are two levels for compatibility - we can ship a dummy dynlink.cma+dynlink.cmxa (and the old My instinct is to do neither of these things since, as we found with OCaml 5.0, the effect of putting them in place is that nothing ever plans for their removal! It's very similar to what happened with bigarray in OCaml 5.0. In Dune, users should be able to specify All that said, this would put an upper bound on all currently released versions of Dune (and any packages not using Dune, whose build systems would also need updating), which might be an argument for doing both - i.e. updating Dune et al to understand that in OCaml 5.4+ I don't mind exactly what we do, but I do (strongly) think we should have a plan which definitely involves there not being a dynlink package/library within a few versions (as opposed to what we've done before where we say we're going to do that and then find we can't because nothing's changed in the ecosystem!). I'm happy to spearhead some of the bulk testing needed to decide on which plan for this but, unlike some of my previous efforts (#11198, #11199, #11200, etc.), I'd prefer to be certain that we're actually going to make the core change ( |
I just looked at 4.14, and the flags are consistent with what I described above. They are, in order:
The -Wl,-E from dynlink is needed so that programs that do use dynlink execute correctly (when using So I have the impression that this behavior doesn't work starting in some unknown ocaml 5 version, given the apparent absence of -Wl,-E flags from dynlink.cmxa in current trunk. But if the absence of -Wl,-E was fixed (I'm guessing there's a $foo missing in a Makefile somewhere), then what I wrote originally should apply: this pull request would prevent dropping the dynamic symbol table, thus forcing larger executables, right? |
Maybe
|
Just to check I understand this correctly - at |
Incidentally, rather than having to be hard-coded in the build system (which is how Dune does it, indeed), this feels more like the configure system:
or, perhaps as a better simile:
|
For my understanding, what's the scenario which means that |
The scenario is: the build system is systematically passing |
The build system knew it was passing |
The build system knows that dynlink.cmxa is on the command line, but that doesn't necessarily imply it's going to be kept. The difference is presumably small in this case, however if dynlink is part of stdlib, the difference between "dynlink is provided on the command line" and "dynlink is kept in the final executable" is ~100% of executables in one case vs ~0% of executables in other case. |
Or leave it to build/configure systems to handle the conditional churn if they want to.
That's for upstream to determine. But six years ago @xavierleroy asked me to write a deprecation procedure for OCaml, the thoughts are still here, including baselines for timings1. Footnotes
|
@xavierleroy I wonder if you have an opinion and would be willing to "shepherd" the PR -- make a general decision and maybe ask someone to review it. (I thought about @nojb maybe.) |
I can do a review of the code changes, if there is agreement on the general direction. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reviewed the first five commits (up to "Move Dynlink to Standard Library") and am fairly convinced that it is a good change. There is only the question of how to handle the deprecation of dynlink.cm(x)a
.
I would split the PR in two to make it easier to review, with one PR with the above five commits and another PR with the rest.
I am in favour of the general direction, as it simplifies a number of tricky things, removes an artificial constrain (putting dynlink
in otherlibs
to work around dependency issues), and these are all good things.
I haven't looked at the code, just read @dra27's excellent description. Overall, I think that sharing more code between the Dynlink library and the toplevel REPLs is a good thing, but I'm not convinced Dynlink belongs to the standard library. For me, the standard library is not "whatever is needed by the compilers", it's "basic functionality that we encourage everyone to use and rely on". Dynlink is a fairly niche functionality, it's hard to use (owing to OCaml's strict link-time consistency checking), and its API is clumsy, so I would not encourage everyone to use it and rely on it. |
Could we consider the opposite solution then, of moving the toplevel outside the "core"? I don't see any obvious downside to that. |
This is confusing me - if Can I ask whether this is a hypothetical build you're referring to, or an actual one, because if it's a real one I'd find it much easier to be looking at it. If it's not a real one, I'm veering towards being less convinced that the change here would be an insurmountable problem. Totally independently of that, but if #13285 has caused flags not to be in |
I'm not sure what I was smoking1. We should obviously not be breaking existing code at the instant of the change. My personal intuition (as with the various attempts I've made at explaining future directions for stdlib-shims) is that the deprecation warning should appear immediately (that doesn't break released code, because of warnings are never treated as errors in releases, right 😉). So I would probably push for Footnotes
|
Thanks for the review of the first part, @nojb - I'm certainly happy to split the subsequent parts off (as a small scheduling matter, I'm away all next week, so delayed replies then will at least be intentional, for a change)! I have mused before if we actually want something more akin to I'm not so sure about removing the toplevel from the boot cycle, @gasche. I'm not sure why, but that doesn't quite feel right to me. However, we could go in a slightly different direction, perhaps, and instead promote I can knock that up as an experimental branch, assuming it's not an instantly repulsive idea to anyone? |
Linking works this way (ignoring
So dynlink.cmxa at the command line doesn't have to be linked in. I agree the flags will be included no matter what, which is strange. But I don't know why we're discussing this. My point is merely that: there are flags attached to dynlink.cmxa (although that got broken somewhere in the 5.x versions), and they should be attached to dynlink.cmx here. The effect of the flags can be mostly emulated by the build system before this pull request, but not after.
I'm talking about an actual build. I don't have access to the build anymore, not that there'd be anything interesting to see. Just imagine https://github.com/ocaml/dune/blob/d826e9b80284794979d57508d80d3d76143d96de/src/dune_rules/exe.ml#L210-L234 listing |
Dynlink
has existed since beginning of OCaml (83c90f9), but the toplevel of course is older, inherited from Caml Light (the oldest I could quickly find was camllight/camllight@610190c!).The bytecode implementation of Dynlink, however, has never been an optional part of the distribution. However, because it used large sections of the bytecode compiler's machinery, it could never have lived in the Standard Library as other runtime features such as
Marshal
have, as it would have pulled most of ocamlbytecomp in with it. That changed as a side-effect of the work of @stedolan and @shindere in #11996, and so there's now no particular reason forDynlink
to remain outside the Standard Library.On its own, that change represents a little bit of ecosystem churn not unlike that caused by the moving of
Bigarray
into the Standard Library in #1685 (althoughDynlink
is a simpler change, since the entire library can move, leaving no need for a compatibility layer), and doesn't necessarily bring that much benefit (although it's arguable "the right thing").However, having moved
Dynlink
into the Standard Library, this opens the possibility of using it for the toplevel. That's presently not possible becauseocaml
is part of the core system, and the core system cannot use otherlibraries1. There's nothing particularly magic about Dynlink, in terms of the physical OCaml code which it consists of - unlike the other libraries in otherlibs, Dynlink's C code already lives entirely in the runtime; it's just the dependencies which were a problem.The first four commits of this therefore merge the existing library into a single module and, having done that, shift it into the Standard Library. The first five commits arguably stand in their own right and, assuming we definitely do want to do this, could be merged separately for 5.4.
The remainder of the PR shows some of what we can then do to the core system, eliminating what to me are some quite unfortunate little corners. In particular, there are two oddities:
caml_dynlink_add_primitive
,caml_dynlink_get_bytecode_sections
, etc.)Both of these peculiarities stem from duplication between Dynlink and some of the core bytecode linking machinery in bytecomp. The remaining commits show that with Dynlink on hand, we can do away with a lot of this duplication.
At present, the magic for getting the bytecode compiler to work in the toplevel centres around three modules in ocamlbytecomp -
Symtable
,Dll
andMeta
. For ocamlc,Symtable
is primarily responsible for tracking C primitives along with tracking the slot numbers for global data. At the end of batch linking, the primitives are checked and the global data is finalised and marshalled into the bytecode image.Symtable
features two initialisation functionsinit
, which is used byocamlc
andinit_toplevel
which is used byocaml
. The latter throws a few internal switches so that instead of merely checking for C primitives,Dll
now actually loads libraries and finds symbol addresses and rather than just tracking and computing the slots for the global table, andSymtable
keeps the VM's copy in sync.Dynlink
duplicates all this latter logic (in dynlink_symtable.ml). The approach I've taken here is to remove the duplicated code fromSymtable
andDll
(which, as a minor side-effect, causesMeta
to be moved to the toplevel, where it belongs, as it's no longer used). In the toplevel, it's then necessary to be able to plumb the two of these together - to have both Dynlink_symtable and the bytecode compiler's Symtable using the same underlying tables (i.e. the opposite of what dynlink_compilerlibs originally solved). There are various mechanisms for doing this. The cleanest is to share fully the typing information between Dynlink and Symtable, but this reveals internal details which it is unfortunate to have to do. While they could go in yet anotherCamlinternal
module, that doesn't feel particular satisfactory either - it's still there. Instead, I propose here a small internal refactoring inSymtable
so that it is able to swap out the three things which needs to be shared withDynlink
- the table of globals, the table of literals and the primitive resolution function (Symtable.of_prim
).Dynlink
therefore registers a value usingCallback.register
and leaves these forSymtable.init_toplevel
to pick up.This plumbing means that the toplevel is now able to use
Symtable
as before to compile phrases that will be able to execute in the current VM. The toplevel then just needs two additional functions (check_global_initialized
andupdate_global_table
). Rather than exposing these functions hidden insideDynlink
, these are simply included along with the global tables. Additionally, the toplevel needs to be able to update the search path (for#directory
), and that then completes the set of data being transferred fromDynlink
to the toplevel viaCallback
.Having done that, ocamlbytecomp now doesn't use any bytecode-specific primitives, which means that we can compile ocamlc.opt without needing dummy stubs in libasmrun - in fact,
runtime/dynlink.c
now looks much more naturally likeruntime/dynlink_byt.c
. Additionally, by threading the Dll search-path machinery in the same way, ocamlnat stops depending on theDll
module of ocamlbytecomp.I have a couple of thoughts for future work which might allow the slight dirtiness of the plumbing to be made a bit more principled:
add_path
andremove_path
) is possibly something which could simply be exposed inDynlink
, just being a no-op for natdynlink (there are already things like this inDynlink
). It's controlling where the runtime searches for stub DLLs mentioned in .cma files, which is something that arguably users ofDynlink
might like to be able to control anyway.Dynlink
to allow bytecode to be linked directly - i.e. to have something akin toDynlink.loadcode
, receiving unpatched bytecode and relocation instructions from the toplevel. That might conceivably be useful for other toplevels on top of our Zinc interpreter. It would also be an interesting interface to explore for having an (optional) bytecode interpreter in native dynlink (which has of course been done before, way back in OCaml 2.x).NB The first five commits represent a "reasonable" story for moving Dynlink into the Standard Library - the remaining commits I'd expect to tidy up if we were to go ahead with these ideas.
Footnotes
technically it's not so much impossible as utterly-unreasonably-complicated-bordering-on-madness ↩