> How would a smaller shared object lead to faster startup? The kernel isn't loading pages to memory unless they're actually used.
IDK on linux but on windows opening a 600mb exe (e.g. release with debug info) and a 15mb one (same but stripped) have a VERY OBVIOUS startup time difference. I assume the OS is scanning the whole binary through some antivirus for instance, and I'd assume the same to be true for DLLs used by the app, so all these kernel loading pages optimizations go through the trash.
That's ridiculous. It's frequent for a process to map large amounts of data from the disk. Requiring that this data be eagerly loaded and be present in memory at all times would be disastrous for performance, if even possible.
Are you using symbol files? I thought most debug info on Windows was put in those, not in the binary itself. We include some debug info with SQL Server, but not symbol files, so that we can debug crashes from production deployments.
The obvious one is a smaller .so may have fewer pages of interest. If each function you call takes half a page, and you call 1/4th of the functions, chances are good you need to load more than 1/4the pages, because sometimes you'll call both functions in a page you load and sometimes you won't. In other words: if the .so is smaller, you get better packing.
The other explanation is prefetching. Lets say all functions take a full page, so packing into a page doesn't matter. When you call a function, which demand pages, chances are the kernel is also going to load adjacent pages; if all the functions you call are adjacent, prefetching saves you significant i/o time. Otoh, if your functions are sparse in the backing file, you will have to wait for i/o; which may be (marginally) delayed because of prefetching on earlier loads.
Users won't touch it directly, but I deem it a given that Chrome and its derivatives upload crash logs to the mothership by default, and IIRC Firefox also offers that as an opt-in. Same for Android (Firebase Crashlytics) and iOS, and I think there's also ways to hook yourself into JS frontend code to transmit uncaught exceptions to a backend server.
The biggest gain seems to come from removing unused zlib functionality which probably would have negligible impact on debugging (if anything it gives the person investigating a bug a list of which specific functions have been used in the library, which seems like it could be pretty helpful!)
Although, thinking about it a bit more, are we sneaking up on the “why not use a static library” zone?
For this particular library, I'm not sure if it's worth it or not. But for some of us working in constrained environments, optimizing binaries for size can make the difference between a binary that'll fit on the box and one that won't, and there were some interesting techniques in the post.
given the proliferation of electron-based desktop apps, which are known to be resource heavy, making various so libraries smaller is indeed a welcome improvement.
Other flags that can be useful not mentioned here:
* -fvisibility=internal (stronger than hidden and must be used with a LOT OF CARE, e.g. never pass a function pointer to a function not marked explicitly with hidden or default visibility in that case as its ABI may change)
* -Bsymbolic / -Bsymbolic-functions / -fno-semantic-interposition (interesting explanations: http://maskray.me/blog/2021-05-09-fno-semantic-interposition)
* -fno-stack-protector (not recommended for libz aha)
* -fno-plt / -fno-ident (very small effect, more for the sake of completeness)
* -fvirtual-function-elimination (surprisingly not done by default, here I can sometime get back a few percent through this on OO heavy codebases)
* -ffunction-sections -fdata-sections -Wl,--gc-sections (largest improvement in my experience, sometimes this literally halved binary size for me when coupled with lto)
* -Wl,--as-needed
* -Wl,--icf=all (I heard horror stories about this but it works fine here)
in some cases, when all used together (as there's some synergy between the various optimizations of these flags) this can have double-digits effects on binary size reduction percentage
Not saying this is the reason, but for large projects it is often done to improve compile times or narrow the interface which consumers of the library can use.
A lot of this is historical and is now very difficult to change without negatively impacting users.
> It is not used by a bunch of other applications besides Firefox so there isn’t much saving from being shared.
In the 2000s it was designed to do so, via XULRunner. You're also forgetting about Thunderbird and SeaMonkey (though I don't think that either of those can actually share libxul.so with Firefox anymore, but I digress).
But as to why it is the case now, there are still compatibility considerations that fall out from the era when Gecko+XUL were written as a runtime environment for hosting applications.
For example, I recall that during the Electrolysis project, we wanted to get rid of the plugin-container executable on MacOS and just host all Firefox child processes using firefox. Some "security" software would break Firefox because it made certain assumptions about the way that various Firefox binaries interacted with each other.
FWIW, Gecko used to consist of a lot more separate libraries, but over the years we recognized that there were a lot of performance issues that we could mitigate by combining them, so most shared libraries were folded into libxul.
To add to this, there were a few cool XUL-based software out there back in the day: Songbird, ActiveState Komodo IDE, Zotero, etc. XUL didn't take over the world like Electron, but for a moment it felt like it could.
It was both too late and not early enough. I 'member back in the VB6 era that you could easily embed Internet Explorer as an ActiveX (or was it OLE?) element right inside your application - a very early sort of WebView, just with absolutely no way to inspect what was going on there. I think Firefox never achieved a 1:1 compatible match for that... despite Microsoft keeping that utter shit around for ages for compatibility reasons as a lot of apps used it under the hood (e.g. HTML Help).
And yes you could absolutely do a Matroshka with it: a VB6 app embedding Internet Explorer and the site embedding an ActiveX element written in VB6. Or inline Java applets.
Funny times... an era in which security was barely a concern. Instead, you had open-ish, usually well documented interfaces for everything.
What killed the idea though until Electron came along was simple: Browsers didn't even come close to feature parity. You had to write your own API layer to communicate between the website and the host application for everything going beyond XHR calls and file selector uploads. Electron took off once Chrome got enough native bridges that you could actually write capable web apps.
It works by keeping an ar archive with PIC objects that would normally go into libc.so.6, determining all symbols used by all programs in the Debian installer, then mapping these symbols to objects, and linking a new libc.so.6 containing only the needed objects.
This is like one of those people who submits pull requests for reformatting code. Completely changed the structure, no idea what effect it will have but just wanted to submit something.
You can’t debug it anymore and you’ve possibly broken something because of the random functions you’ve pruned but you won’t be able to tell what you’ve broken. All to save literally a few bytes.
31 comments
[ 3.3 ms ] story [ 41.8 ms ] threadWhat's the point of making the so smaller to begin with?
99.9% of users will never touch all this, in return they get faster downloads of the browser and faster startup.
Yes, I suppose for pre release versions the calculation is different, but for a stable release there 'shouldn't' be any bugs.
You need some of that debug information to send backtraces back to the developers.
IDK on linux but on windows opening a 600mb exe (e.g. release with debug info) and a 15mb one (same but stripped) have a VERY OBVIOUS startup time difference. I assume the OS is scanning the whole binary through some antivirus for instance, and I'd assume the same to be true for DLLs used by the app, so all these kernel loading pages optimizations go through the trash.
Even Windows wouldn't do something so silly.
The obvious one is a smaller .so may have fewer pages of interest. If each function you call takes half a page, and you call 1/4th of the functions, chances are good you need to load more than 1/4the pages, because sometimes you'll call both functions in a page you load and sometimes you won't. In other words: if the .so is smaller, you get better packing.
The other explanation is prefetching. Lets say all functions take a full page, so packing into a page doesn't matter. When you call a function, which demand pages, chances are the kernel is also going to load adjacent pages; if all the functions you call are adjacent, prefetching saves you significant i/o time. Otoh, if your functions are sparse in the backing file, you will have to wait for i/o; which may be (marginally) delayed because of prefetching on earlier loads.
Users won't touch it directly, but I deem it a given that Chrome and its derivatives upload crash logs to the mothership by default, and IIRC Firefox also offers that as an opt-in. Same for Android (Firebase Crashlytics) and iOS, and I think there's also ways to hook yourself into JS frontend code to transmit uncaught exceptions to a backend server.
Although, thinking about it a bit more, are we sneaking up on the “why not use a static library” zone?
It is not used by a bunch of other applications besides Firefox so there isn’t much saving from being shared.
In addition, there isn’t a scenario where libxul would be released independently of Firefox for a security issue, so there is no advantage there.
You lose a lot of optimization opportunities with shared libraries and increase complexity.
> Why is libxul a shared library?
A lot of this is historical and is now very difficult to change without negatively impacting users.
> It is not used by a bunch of other applications besides Firefox so there isn’t much saving from being shared.
In the 2000s it was designed to do so, via XULRunner. You're also forgetting about Thunderbird and SeaMonkey (though I don't think that either of those can actually share libxul.so with Firefox anymore, but I digress).
But as to why it is the case now, there are still compatibility considerations that fall out from the era when Gecko+XUL were written as a runtime environment for hosting applications.
For example, I recall that during the Electrolysis project, we wanted to get rid of the plugin-container executable on MacOS and just host all Firefox child processes using firefox. Some "security" software would break Firefox because it made certain assumptions about the way that various Firefox binaries interacted with each other.
FWIW, Gecko used to consist of a lot more separate libraries, but over the years we recognized that there were a lot of performance issues that we could mitigate by combining them, so most shared libraries were folded into libxul.
And yes you could absolutely do a Matroshka with it: a VB6 app embedding Internet Explorer and the site embedding an ActiveX element written in VB6. Or inline Java applets.
Funny times... an era in which security was barely a concern. Instead, you had open-ish, usually well documented interfaces for everything.
What killed the idea though until Electron came along was simple: Browsers didn't even come close to feature parity. You had to write your own API layer to communicate between the website and the host application for everything going beyond XHR calls and file selector uploads. Electron took off once Chrome got enough native bridges that you could actually write capable web apps.
It works by keeping an ar archive with PIC objects that would normally go into libc.so.6, determining all symbols used by all programs in the Debian installer, then mapping these symbols to objects, and linking a new libc.so.6 containing only the needed objects.
You can’t debug it anymore and you’ve possibly broken something because of the random functions you’ve pruned but you won’t be able to tell what you’ve broken. All to save literally a few bytes.