Skip to content

Conversation

@xal-0
Copy link
Member

@xal-0 xal-0 commented Nov 25, 2025

Some testing by @BenChung has revealed how hard it is to load a JuliaC-built library into a program that has already loaded a very old version of libstdc++. Even with probing disabled, libjulia-internal.so fails because of missing GLIBCXX symbols.

We use so little of the C++ standard library in libjulia-internal.so that it's worth the tradeoff to link it statically: it barely changes the size of the resulting library, removes a medium-size library we have to ship in trimmed bundles, and solves some of our hermeticity issues when being loaded by other software. libjulia-codegen.so uses it more extensively, and we expect to be able to load it as a plugin for opt, meaning it may have to remain dynamically linked.

This PR contains a series of changes to enable statically linked libstdc++ by default and mitigate the size impact:

  • We enable --gc-sections when building with gcc/ld.bfd. This saves us some code already, since we can trim out a lot of libLLVMSupport/libLLVMTargetParser. On macOS, we can use -dead_strip to save some space even though the rest of these changes are not applicable.
  • Two flags for -static-libstdc++ and -static-libgcc, called USE_RT_STATIC_LIBSTDCXX and USE_RT_STATIC_LIBGCC, are added and enabled by default.
  • Most of the additional code that gets linked into libjulia-internal.so is related to locales for iostreams. Replace these with standard C IO and LLVM helpers that don't have weird locale-related behaviour.
  • (NOT IMPLEMENTED) --gc-sections removes unused executable code, but leaves us with a lot of irrelevant debug info. I tested the effect of llvm-dwarfutil --garbage-collection and saw pretty good savings, but that is not included in this PR because BinaryBuilder doesn't have a new enough version of the LLVM tools.
Change Size of libjulia-internal.so (KiB)
No change 14524
Linker GC enabled 13220
-static-libstdc++ and -static-libgcc 22136
Excise iostreams 15036
DWARF GC (not in this PR) 11488

This is a comparison of symbols that were added and removed. Even though 15 times more code is removed than added, the resulting libjulia-internal.so has a similar size to the original because of the additional debug info.

WHERE     NUM      SIZE
both     4678
removed  3727   1188754
added     448     78255

Largest added:
    17336 d_print_comp_inner
     2667 d_type
     2368 d_print_mod
     2122 execute_cfa_program
     2100 d_exprlist
     1862 execute_stack_op
     1785 search_object
     1691 d_expression_1
     1672 uw_frame_state_for
     1658 d_encoding
     1632 cplus_demangle_operators
     1442 d_demangle_callback.constprop.0
     1419 d_name
     1351 __gxx_personality_v0
     1168 _Unwind_IteratePhdrCallback
     1163 d_unqualified_name
     1088 cplus_demangle_builtin_types
     1074 d_print_mod_list
     1064 uw_update_context_1
      944 d_maybe_print_fold_expression.isra.0
      829 d_print_function_type.isra.0
      760 _Unwind_RaiseException
      729 d_print_array_type.isra.0
      680 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocat…
      617 llvm::support::detail::provider_format_adapter<int&>::format(llvm::ra…
      617 llvm::support::detail::provider_format_adapter<unsigned long&>::forma…
      571 d_cv_qualifiers
      562 _Unwind_Find_FDE
      531 d_substitution
      417 d_operator_name
      415 uw_install_context_1
      408 _Unwind_ForcedUnwind
      392 standard_subs
      392 _Unwind_Resume
      384 frame_hdr_cache
      373 uw_init_context_1
      369 linear_search_fdes
      369 d_expr_primary
      339 __cxa_demangle
      339 d_source_name
      331 classify_object_over_fdes
      322 read_encoded_value_with_base(unsigned char, unsigned long, unsigned c…
      322 read_encoded_value_with_base
      314 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocat…
      309 __cxxabiv1::__si_class_type_info::__do_dyncast(long, __cxxabiv1::__cl…
      299 add_fdes
      290 __deregister_frame_info_bases
      290 get_cie_encoding
      287 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocat…
      284 (anonymous namespace)::pool::free(void*) (.constprop.0)

Largest removed:
    12115 llvm::cl::ParseCommandLineOptions(int, char const* const*, llvm::Stri…
    12115 llvm::cl::ParseCommandLineOptions(int, char const* const*, llvm::Stri…
    11449 Execute(llvm::sys::ProcessInfo&, llvm::StringRef, llvm::ArrayRef<llvm…
    10310 llvm::DisplayGraph(llvm::StringRef, bool, llvm::GraphProgram::Name)
     8071 llvm::itanium_demangle::AbstractManglingParser<llvm::itanium_demangle…
     8018 gc_collect_neighbors
     6652 llvm::ARM::getDefaultExtensions(llvm::StringRef, llvm::ARM::ArchKind)
     6572 void std::__introsort_loop<__gnu_cxx::__normal_iterator<llvm::TimerGr…
     6509 llvm::vfs::RedirectingFileSystemParser::parseEntry(llvm::yaml::Node*,…
     6116 printSymbolizedStackTrace(llvm::StringRef, void**, int, llvm::raw_ost…
     5820 llvm::ARM::getDefaultFPU(llvm::StringRef, llvm::ARM::ArchKind)
     5820 llvm::ARM::getDefaultFPU(llvm::StringRef, llvm::ARM::ArchKind) (.loca…
     5688 llvm::sys::unicode::isPrintable(int)::PrintableRanges
     5508 llvm::itanium_demangle::AbstractManglingParser<llvm::itanium_demangle…
     5485 (anonymous namespace)::CommandLineCommonOptions::CommandLineCommonOpt…
     5485 (anonymous namespace)::CommandLineCommonOptions::CommandLineCommonOpt…
     5289 llvm::sys::detail::getHostCPUNameForARM(llvm::StringRef)
     4886 llvm::sys::Wait(llvm::sys::ProcessInfo const&, std::optional<unsigned…
     4886 llvm::sys::Wait(llvm::sys::ProcessInfo const&, std::optional<unsigned…
     4872 llvm::DebugCounter::print(llvm::raw_ostream&) const
     4872 llvm::DebugCounter::print(llvm::raw_ostream&) const (.localalias)
     4828 llvm::cl::ExpansionContext::expandResponseFiles(llvm::SmallVectorImpl…
     4828 llvm::cl::ExpansionContext::expandResponseFiles(llvm::SmallVectorImpl…
     4811 void std::__introsort_loop<llvm::SMFixIt*, long, __gnu_cxx::__ops::_I…
     4647 llvm::yaml::Document::parseBlockNode()
     4647 llvm::yaml::Document::parseBlockNode() (.localalias)
     4643 llvm::detail::IEEEFloat::toString(llvm::SmallVectorImpl<char>&, unsig…
     4643 llvm::detail::IEEEFloat::toString(llvm::SmallVectorImpl<char>&, unsig…
     4417 parseArch(llvm::StringRef)
     4399 llvm::vfs::RedirectingFileSystemParser::parse(llvm::yaml::Node*, llvm…
     4300 llvm::APIntOps::SolveQuadraticEquationWrap(llvm::APInt, llvm::APInt, …
     4008 llvm::itanium_demangle::AbstractManglingParser<llvm::itanium_demangle…
     3916 llvm::Triple::normalize[abi:cxx11](llvm::StringRef, llvm::Triple::Can…
     3645 void std::__introsort_loop<__gnu_cxx::__normal_iterator<llvm::vfs::YA…
     3524 llvm::xxh3_128bits(llvm::ArrayRef<unsigned char>)
     3432 RedirectIO(std::optional<llvm::StringRef>, int, std::__cxx11::basic_s…
     3203 llvm::yaml::escape[abi:cxx11](llvm::StringRef, bool)
     3142 llvm::yaml::dumpTokens(llvm::StringRef, llvm::raw_ostream&)
     3142 llvm::vfs::RedirectingFileSystem::dir_begin(llvm::Twine const&, std::…
     3142 llvm::vfs::RedirectingFileSystem::dir_begin(llvm::Twine const&, std::…
     3129 llvm::TimerGroup::PrintQueuedTimers(llvm::raw_ostream&) (.localalias)
     3129 llvm::TimerGroup::PrintQueuedTimers(llvm::raw_ostream&)
     3007 (anonymous namespace)::CategorizedHelpPrinter::printOptions(llvm::Sma…
     2966 llvm::TimerGroup::TimerGroup(llvm::StringRef, llvm::StringRef, llvm::…
     2966 llvm::TimerGroup::TimerGroup(llvm::StringRef, llvm::StringRef, llvm::…
     2908 llvm::DebugCounter::push_back(std::__cxx11::basic_string<char, std::c…
     2908 llvm::DebugCounter::push_back(std::__cxx11::basic_string<char, std::c…
     2877 llvm::itanium_demangle::AbstractManglingParser<llvm::itanium_demangle…
     2855 llvm::yaml::Node::getVerbatimTag[abi:cxx11]() const
     2851 llvm::vfs::RedirectingFileSystem::create(llvm::ArrayRef<std::pair<std…

@topolarity
Copy link
Member

topolarity commented Nov 25, 2025

This is great!

Size of libjulia-internal.so (KiB)

Can you show the stripped / non-stripped sizes?

@xal-0
Copy link
Member Author

xal-0 commented Nov 25, 2025

Change strip -s size (KiB)
No change 3544
Linker GC enabled 2424
-static-libstdc++ and -static-libgcc 2964
Excise iostreams 2516

@xal-0 xal-0 marked this pull request as ready for review November 26, 2025 00:37
@topolarity topolarity self-requested a review November 26, 2025 15:51
# NB: CG needs uv_mutex_* symbols, but we expect to export them from libjulia-internal
CG_LIBS := $(LIBUNWIND) $(CG_LLVMLINK) $(OSLIBS) $(LIBTRACYCLIENT) $(LIBITTAPI)

ifeq ($(USEGCC),1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the USEGCC guard necessary?

I thought these flags were semi-standard at this point

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang doesn't seem to mind if you pass -static-libstdc++ when linking to libc++ (seems to just ignore it), but it does error on -static-libgcc if we wouldn't normally link it.

Copy link
Member

@topolarity topolarity left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look good to me. Definitely seems like the right move.

This will significantly reduce our surface area for libstdc++ conflicts, and it's impressive you were able to win almost all of the file size back as well 👍

@topolarity topolarity merged commit f36882f into JuliaLang:master Nov 27, 2025
9 checks passed
@xal-0 xal-0 deleted the libjulia-static-libstdcpp branch November 27, 2025 03:47
@nalimilan
Copy link
Member

Apparently this broke compilation here on Fedora 41 (gcc version 14.3.1):

/usr/bin/ld: cannot find -lstdc++: No such file or directory
collect2: error: ld returned 1 exit status
make[1]: *** [Makefile:467: /home/milan/Dev/julia/usr/lib/libjulia-internal.so.1.14.0] Error 1
make: *** [Makefile:118: julia-src-release] Error 2

It works with USE_RT_STATIC_LIBSTDCXX=0. Any idea what may be going on?

@xal-0
Copy link
Member Author

xal-0 commented Dec 1, 2025

You are probably missing the libstdc++-static package. We'll need to add that to the devdocs.

@nalimilan
Copy link
Member

Ah of course. gcc could print a more explicit error... But yeah it would be worth adding to the docs.

vtjnash pushed a commit that referenced this pull request Dec 2, 2025
Red Hat-based distros don't include the static version of libstdc++ in
their g++ packages, so document this dependency, as well as the
`USE_RT_STATIC_LIBSTDCXX=0` workaround if it isn't available. I also
looked for missing dependencies by compiling Julia in the base
`fedora:42` and `debian:12` images:

```dockerfile
FROM debian:12 AS build

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential libatomic1 python3 gfortran perl wget m4 cmake pkg-config \
    curl git ca-certificates

WORKDIR /build
RUN git clone --depth=1 https://github.com/JuliaLang/julia.git

RUN cd julia && make -j binary-dist VERBOSE=1
```

```dockerfile
FROM fedora:42 AS build

RUN dnf install -y --nodocs --setopt=install_weak_deps=False \
    gcc gcc-c++ gcc-gfortran python3 perl wget m4 cmake pkgconfig curl git \
    which diffutils libatomic libstdc++-static

WORKDIR /build
RUN git clone --depth=1 https://github.com/JuliaLang/julia.git

RUN cd julia && make -j binary-dist VERBOSE=1
```

Reported by
#60248 (comment).
topolarity added a commit that referenced this pull request Dec 3, 2025
Somewhat of a companion to
#60248.

For a small application that has just started up `fork()` is not a huge
concern, but it's quite heavy-handed for Julia- as-a-library scenarios
where resident memory may already be large. Many soft-embedded targets
also do not support fork() well, so it is good for our compatibility to
adjust this.

Rather than relying on the linker to do all of the heavy lifting, this
changes our `libstdcxx` probe sequence to directly parse the
`ld.so.cache` and `libstdc++.so.6` files. As long as we can expect
`/etc/ld.so.cache` to be the same path on all Linux systems, this seems
to be a reliable way to locate system libraries.
KristofferC pushed a commit that referenced this pull request Dec 4, 2025
Some testing by @BenChung has revealed how hard it is to load a
JuliaC-built library into a program that has already loaded a very old
version of libstdc++. Even with probing disabled, `libjulia-internal.so`
fails because of missing GLIBCXX symbols.

We use so little of the C++ standard library in `libjulia-internal.so`
that it's worth the tradeoff to link it statically: it barely changes
the size of the resulting library, removes a medium-size library we have
to ship in trimmed bundles, and solves some of our hermeticity issues
when being loaded by other software. `libjulia-codegen.so` uses it more
extensively, and we expect to be able to load it as a plugin for `opt`,
meaning it may have to remain dynamically linked.

This PR contains a series of changes to enable statically linked
libstdc++ by default and mitigate the size impact:
- We enable `--gc-sections` when building with gcc/ld.bfd. This saves us
some code already, since we can trim out a lot of
libLLVMSupport/libLLVMTargetParser. On macOS, we can use `-dead_strip`
to save some space even though the rest of these changes are not
applicable.
- Two flags for `-static-libstdc++` and `-static-libgcc`, called
`USE_RT_STATIC_LIBSTDCXX` and `USE_RT_STATIC_LIBGCC`, are added and
enabled by default.
- Most of the additional code that gets linked into
`libjulia-internal.so` is related to locales for iostreams. Replace
these with standard C IO and LLVM helpers that don't have weird
locale-related behaviour.
- (NOT IMPLEMENTED) `--gc-sections` removes unused executable code, but
leaves us with a lot of irrelevant debug info. I tested the effect of
`llvm-dwarfutil --garbage-collection` and saw pretty good savings, but
that is not included in this PR because BinaryBuilder doesn't have a new
enough version of the LLVM tools.

| Change | Size of `libjulia-internal.so` (KiB) |

|--------------------------------------|--------------------------------------|
| No change | 14524 |
| Linker GC enabled | 13220 |
| -static-libstdc++ and -static-libgcc | 22136 |
| Excise iostreams | 15036 |
| DWARF GC (not in this PR) | 11488 |

This is a comparison of symbols that were added and removed. Even though
15 times more code is removed than added, the resulting
`libjulia-internal.so` has a similar size to the original because of the
additional debug info.

(cherry picked from commit f36882f)
KristofferC pushed a commit that referenced this pull request Dec 4, 2025
Somewhat of a companion to
#60248.

For a small application that has just started up `fork()` is not a huge
concern, but it's quite heavy-handed for Julia- as-a-library scenarios
where resident memory may already be large. Many soft-embedded targets
also do not support fork() well, so it is good for our compatibility to
adjust this.

Rather than relying on the linker to do all of the heavy lifting, this
changes our `libstdcxx` probe sequence to directly parse the
`ld.so.cache` and `libstdc++.so.6` files. As long as we can expect
`/etc/ld.so.cache` to be the same path on all Linux systems, this seems
to be a reliable way to locate system libraries.

(cherry picked from commit ac4ee59)
KristofferC pushed a commit that referenced this pull request Dec 4, 2025
Some testing by @BenChung has revealed how hard it is to load a
JuliaC-built library into a program that has already loaded a very old
version of libstdc++. Even with probing disabled, `libjulia-internal.so`
fails because of missing GLIBCXX symbols.

We use so little of the C++ standard library in `libjulia-internal.so`
that it's worth the tradeoff to link it statically: it barely changes
the size of the resulting library, removes a medium-size library we have
to ship in trimmed bundles, and solves some of our hermeticity issues
when being loaded by other software. `libjulia-codegen.so` uses it more
extensively, and we expect to be able to load it as a plugin for `opt`,
meaning it may have to remain dynamically linked.

This PR contains a series of changes to enable statically linked
libstdc++ by default and mitigate the size impact:
- We enable `--gc-sections` when building with gcc/ld.bfd. This saves us
some code already, since we can trim out a lot of
libLLVMSupport/libLLVMTargetParser. On macOS, we can use `-dead_strip`
to save some space even though the rest of these changes are not
applicable.
- Two flags for `-static-libstdc++` and `-static-libgcc`, called
`USE_RT_STATIC_LIBSTDCXX` and `USE_RT_STATIC_LIBGCC`, are added and
enabled by default.
- Most of the additional code that gets linked into
`libjulia-internal.so` is related to locales for iostreams. Replace
these with standard C IO and LLVM helpers that don't have weird
locale-related behaviour.
- (NOT IMPLEMENTED) `--gc-sections` removes unused executable code, but
leaves us with a lot of irrelevant debug info. I tested the effect of
`llvm-dwarfutil --garbage-collection` and saw pretty good savings, but
that is not included in this PR because BinaryBuilder doesn't have a new
enough version of the LLVM tools.

| Change | Size of `libjulia-internal.so` (KiB) |

|--------------------------------------|--------------------------------------|
| No change | 14524 |
| Linker GC enabled | 13220 |
| -static-libstdc++ and -static-libgcc | 22136 |
| Excise iostreams | 15036 |
| DWARF GC (not in this PR) | 11488 |

This is a comparison of symbols that were added and removed. Even though
15 times more code is removed than added, the resulting
`libjulia-internal.so` has a similar size to the original because of the
additional debug info.

(cherry picked from commit f36882f)
KristofferC pushed a commit that referenced this pull request Dec 4, 2025
Somewhat of a companion to
#60248.

For a small application that has just started up `fork()` is not a huge
concern, but it's quite heavy-handed for Julia- as-a-library scenarios
where resident memory may already be large. Many soft-embedded targets
also do not support fork() well, so it is good for our compatibility to
adjust this.

Rather than relying on the linker to do all of the heavy lifting, this
changes our `libstdcxx` probe sequence to directly parse the
`ld.so.cache` and `libstdc++.so.6` files. As long as we can expect
`/etc/ld.so.cache` to be the same path on all Linux systems, this seems
to be a reliable way to locate system libraries.

(cherry picked from commit ac4ee59)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants