Creating and Linking to Shared Library Version of Go Runtime?

961 views
Skip to first unread message

jlfo...@berkeley.edu

unread,
Jan 28, 2023, 2:26:55 PM1/28/23
to golang-nuts
For people like me who have no intention of ever distributing a Go executable, I wonder
if it would be useful, and possible, to link to a shared library version of the Go
Runtime. This would make my binaries much smaller and would reduce ware and
tear on my SSD.

Of course, this presumes that such a shared library could be created in the first place.

I did a quick Google and I didn't find this issue being previously discussed.

Comments?

Cordially,
Jon Forrest



Ian Lance Taylor

unread,
Jan 28, 2023, 10:51:25 PM1/28/23
to jlfo...@berkeley.edu, golang-nuts
At least on Linux systems it should work to run "go install
-buildmode=shared std" and then to build other Go programs with
"-linkshared".

Ian

jlfo...@berkeley.edu

unread,
Jan 28, 2023, 11:28:53 PM1/28/23
to golang-nuts
Thanks for the reply. I had mixed results.

On Fedora 37, go version go1.19.4 linux/amd64, in /usr/local/go/src as root I ran

go install -buildmode=shared std

That seemed to work.

In my normal working directory, as me, not root, I ran

go build -linkshared *.go

(I do a regular build in this directory by just running "go build *.go")

That resulted in a whole bunch of error messages of the style

go build internal/goarch: copying /tmp/go-build2481653269/b006/_pkg_.a: open /usr/local/go/pkg/linux_amd64_dynlink/internal/goarch.a: permission denied

So I became root and ran go build -linkshared *.go again.

This time it worked!! The result was 83584 byte binary, whereas the binary produced
by the standard method is 4186764 bytes. That's a great savings!!! The small binary seemed
to work fine.

Just for yuks, I tried building my program as me again (not root). I got permission error messages
again, but now they look like

open /usr/local/go/pkg/linux_amd64_dynlink/archive/tar.shlibname: permission denied

There are 238 such lines.

There's another problem. Unlike what I would expect, it takes *longer* to build the shared version
than the static version.

As root

[root@localhost]# time go build *.go

real    0m0.298s
user    0m0.346s
sys     0m0.091s

[root@localhost]# time go build -linkshared *.go

real    0m1.441s
user    0m1.193s
sys     0m0.325s

That doesn't seem right.

Any advice?

Cordially,
Jon Forrest

Kurtis Rader

unread,
Jan 28, 2023, 11:46:40 PM1/28/23
to jlfo...@berkeley.edu, golang-nuts
It does not surprise me that your shared run-time build takes more time than the usual static build. The latter case is highly optimized while your use case has probably been given very little consideration. I am also confused by your argument for supporting linking against a shared Go runtime library. You wrote earlier that the reason you want this is to "reduce ware (sic) and tear on my SSD." I see no reason why linking against a shared Go run-time library would reduce the "wear and tear" on your SSD. I think your premise is flawed.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/4dc94780-06e6-4aa3-a9b1-64b97dd85a5en%40googlegroups.com.


--
Kurtis Rader
Caretaker of the exceptional canines Junior and Hank

Robert Engels

unread,
Jan 29, 2023, 1:01:42 AM1/29/23
to Kurtis Rader, jlfo...@berkeley.edu, golang-nuts
Shared dynamic libraries do reduce the “wear and tear” on an SSD.  The binaries are loaded a single time and shared across processes - slightly longer startup times for the dynamic linkage. 

It is highly beneficial with large runtimes vs small standard library usage in tiny utilities.  

On Jan 28, 2023, at 10:46 PM, Kurtis Rader <kra...@skepticism.us> wrote:



jake...@gmail.com

unread,
Jan 29, 2023, 8:58:40 AM1/29/23
to golang-nuts
This is pretty OT, and I am no expert, but the overwhelming consensus on the inter-tubes seems to be that reading from an SSD causes no 'wear' whatsoever. It is only writes and deletes that age an SSD. So this use case should not impact SSD life.

But it is an interesting endeavor on its own.

jlfo...@berkeley.edu

unread,
Jan 29, 2023, 12:49:17 PM1/29/23
to golang-nuts
The discussion of SSD wear and tear is going way off my original question. I'm sorry I even mentioned it.
Can we drop it?

I'm still interested in the answer to the original question, which we've made some progress in answering.
As of now the answer is yes, subject to some permission and performance problems. I hope the experts
can shed some light on these.

Cordially,
Jon Forrest

bobj...@gmail.com

unread,
Jan 29, 2023, 3:26:51 PM1/29/23
to golang-nuts
I'm glad to see this issue getting some discussion. I have 100+ smallish utility programs written in Go, and each one consumes about 1.5 MB (precise average: 1,867,844 bytes); my bin directory contains 100+ copies of the Go runtime. Sadly, I mainly use Windows, and there seems to be no way to use linked libraries in Go for Windows.

My solution has been to rewrite many of my smallish Go programs in Python (except those that really need Go's speed)  -- 10K each vs. 1.5M each disk storage. For these  manually invoked utilities, the speed difference is often not noticeable. And the number of source lines and overall program complexity is reduced by roughly 30%. (Added benefit: the Python programs are always properly indented, even without a "pyfmt" program :-)

I *am* a Go fan, and I understand that Go's mission is for writing big server programs, but it's too bad that the size of a small Go program executable is *many* times larger than a small C program.

TheDiveO

unread,
Jan 29, 2023, 4:42:11 PM1/29/23
to golang-nuts
Are the std/runtime .so's even versioned? How do you manage that?

Every time I'm feeling like finally being in $PARADISE out of the .so dependency $VERYVERYHOTPLACE there comes along the demand to go back. Sigh. ;)

Brian Candler

unread,
Jan 30, 2023, 3:10:54 AM1/30/23
to golang-nuts
Here are some more options you could consider.

1. Do what busybox does: put them all into separate packages, link them all into one monster executable, switching on os.Args[0] to decide which main function to run.  Then add lots of links or symlinks to the same program so it can be executed under multiple names.

To make this more modular you could build the packages as go plugins, although there are a lot of limitations to beware of:
https://www.reddit.com/r/golang/comments/b6h8qq/is_anyone_actually_using_go_plugins/ejkxd2k/?utm_source=reddit&utm_medium=web2x&context=3

2. Use one of several Go interpreters out there. However I'd expect execution speed to be less than python, which has had years of optimisation.

3. There's tinygo - although it doesn't support Windows AFAIK, maybe the Linux binaries could run under WSL.

It's a substantial subset of go, and by default achieves binaries less than 0.2MB.  If you turn on a bunch of options (such as disabling goroutines and the garbage collector) and avoid "fmt" you can get really tiny standalone binaries:

brian@builder:~$ cat hello1.go
package main

import "fmt"

func main() {
        fmt.Println("Hello, world!")
}
brian@builder:~$ cat hello2.go
package main

//import "fmt"

func main() {
        println("Hello, world!")
}
brian@builder:~$ tinygo build -o hello1 hello1.go
brian@builder:~$ tinygo build -o hello2 -no-debug -panic=trap -scheduler=none -gc=leaking hello2.go
brian@builder:~$ ls -l hello1 hello2
-rwxrwxr-x 1 brian brian 171408 Jan 30 08:05 hello1
-rwxrwxr-x 1 brian brian   7800 Jan 30 08:05 hello2


fge...@gmail.com

unread,
Jan 30, 2023, 4:43:01 AM1/30/23
to bobj...@gmail.com, golang-nuts
On 1/29/23, bobj...@gmail.com <bobj...@gmail.com> wrote:
> I'm glad to see this issue getting some discussion. I have 100+ smallish
> utility programs written in Go, and each one consumes about 1.5 MB (precise
>
> average: 1,867,844 bytes); my bin directory contains 100+ copies of the Go
> runtime. Sadly, I mainly use Windows, and there seems to be no way to use
> linked libraries in Go for Windows.
>
> My solution has been to rewrite many of my smallish Go programs in Python
> (except those that really need Go's speed) -- 10K each vs. 1.5M each disk
> storage.
...
I've seen similar reasoning before, hence my question: can you share
some details about your windows environment where ~150MB difference
for 100+ programs in storage needs is noticeable?
thanks!

jlfo...@berkeley.edu

unread,
Jan 30, 2023, 3:55:49 PM1/30/23
to golang-nuts
I'm the original poster. I've looked into this more and I might have an explanation for
what's going on.

Just for yuks, I started with perhaps the simplest Go program, which I called t.go:

package main
func main() {
}

As root, I was able to build both a dynamically (21512 bytes) and a statically (1203019 bytes) linked  executable.
This is with go version go1.19.4 linux/amd64.

I then ran

go build -linkshared -x t.go

This showed all the commands the go tool executed. I saved these commands in a file
so I could look more closely at what was going on.

Here's what I think is going on. When running go build -linkshared go attempts to create a new
version of the shared runtime library. At first glance, it appears that only the code that's necessary
to run the executable is put there (I'm not 100% sure about this). But it's clear that a new file
called

/usr/local/go/pkg/linux_amd64_dynlink/libstd.so

is created. As a normal user I create or write to this file, which is why I get all
the permission denied error messages. So, the main question is why does go build create
this library when I already ran

go install -buildmode=shared std

to create the shared library?

To continue, as root I changed the ownership of

/usr/local/go/pkg/linux_amd64_dynlink

to me, and then, as me, was finally able to create the dynamically linked executable!

So, to answer my original question, the reason I can't create a dynamically linked
executable is because the go build tool attempts to create a new shared library in
a location that a normal user can't write to. Whether it's actually necessary to
create this shared library I can't say, but if so, it should be done an a directory
that a normal user can write to.

This also answers the second question, which is why it takes longer to create a dynamically
linked executable than a statically linked executable. This is due to the time it takes to
build the possibly-extraneous shared library.

I welcome any comments and/or corrections to any of this.

Cordially,
Jon Forrest

Jim Idle

unread,
Jan 30, 2023, 8:54:35 PM1/30/23
to jlfo...@berkeley.edu, golang-nuts
Looking back at your posts, I think you got there because your initial build of the shared library was via sudo? I don’t think that was correct and you didn’t need sudo.

If the shared library only contained code needed for your executable, then it could only be shared by other instances of your executable. 

Jim

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

jlfo...@berkeley.edu

unread,
Jan 30, 2023, 9:37:39 PM1/30/23
to golang-nuts
Thanks for the comments. I had to do it as root because running

go install -buildmode=shared std

as me results in

go install internal/goarch: mkdir /usr/local/go/pkg/linux_amd64_dynlink: permission denied

As root, changing the ownership of /usr/local/go/pkg/linux_amd64_dynlink to me, and then, as me, running

go install -buildmode=shared std

ran to completion. So, the solution to problem #1 is to make sure a normal user can create
/usr/local/go/pkg/linux_amd64_dynlink and files in and underneath it.

(I did all this after reinstalling go from scratch).

However, none of this changed the possible creation of an possibly unnecessary  shared lib, and the
resulting build time penalty, e.g

% time go build -linkshared t.go
go build -linkshared t.go  1.22s user 0.29s system 103% cpu 1.455 total
% time go build t.go
go build t.go  0.10s user 0.04s system 132% cpu 0.102 total

In looking at the commands executed by go build, I still see what looks like a lot
of extra work being done, but I'm not 100% sure. That's what I'm hoping a Go
expert can comment on. (I'd be happy to send the list of commands being executed
to anybody who wants it. )

I agree it wouldn't make any sense for a shared library to be created that only contains
the code that my program needs. I was expecting for a shared library to be created that
contains the entire go runtime.

 Jon



TheDiveO

unread,
Jan 31, 2023, 4:48:20 PM1/31/23
to golang-nuts
> So, the solution to problem #1 is to make sure a normal user can create
/usr/local/go/pkg/linux_amd64_dynlink and files in and underneath it.

While this is fine as a proof of concept and to diagnose the situation, I don't think that it can be called good advice to make /usr/local writeable to even one ordinary user ... or am I mistaking here something?

jlfo...@berkeley.edu

unread,
Jan 31, 2023, 5:37:37 PM1/31/23
to golang-nuts
I agree with you 100%. The 'go install' step that creates the shared runtime library should only be done by
root.

On the other hand, there's no reason I can see for the 'go build' step to write anything at all
into /usr/local/go. But that's clearly happening. That's one of the causes of this problem.

Jon




TheDiveO

unread,
Feb 2, 2023, 5:09:06 PM2/2/23
to golang-nuts
If I read the 1.20 release notes correctly, there has been a change with how the compiled std lib not only is delivered (not anymore) and cached, so that it now ends up in the module cache. Maybe you can retry your experiments with 1.20 if this now works without the slightly ugly workarounds?

jlfo...@berkeley.edu

unread,
Feb 3, 2023, 2:00:25 PM2/3/23
to golang-nuts
Good idea.

FYI - this is on a Fedora 37 6.1.8-200.fc37.x86_64 server with go version go1.20 linux/amd64.
I'm running a VM in Virtualbox with snapshots so it's very easy to go back to an unmodified
system after running an experiment. Note that I'm adding the '-a' option to the go build commands
so that go doesn't use cached versions of things.

The test program (t.go) is

package main

func main() {
}

Without having run go install -buildmode=shared std, go build -linkshared -a t.go produces

/usr/local/go/pkg/tool/linux_amd64/link: cannot implicitly include runtime/cgo in a shared library

I think this is a new error message. I didn't expect this command to succeed but I was curious how it
would fail.

I then ran go install -buildmode=shared std which again ran without any error messages.

Running go build  -linkshared -a t.go now produces many error messages, all complaining about
not being able to write to various subdirectories under /usr/local/go/pkg/linux_amd64_dynlink.
Why running go build as a normal user results in writes to /usr/local/go/pkg/linux_amd64_dynlink
is indeed a puzzlement.

To see whether the time difference problem still exists, I then changed the ownership of
/usr/local/go/pkg/linux_amd64_dynlink to me. In ordinarily life I would never do this but
this is in a test VM.

Now, running go build  -linkshared -a t.go works! However, the slow build time when building
with -sharedlink still exists.

% time go build  -linkshared -a t.go
go build -linkshared -a t.go  43.57s user 6.09s system 262% cpu 18.928 total
% time go build -a t.go
go build -a t.go  4.26s user 0.53s system 174% cpu 2.750 total

This is slower than in my original report because I'm using the '-a' option to eliminate any
benefit of the cache. Running without it is much faster but the relative time difference
(e.g ~10x) still exists.

So, to answer your question, things don't appear to have changed with Go 1.20.

Cordially,
Jon Forrest
Reply all
Reply to author
Forward
0 new messages