Usages of the sigdelay module can be significantly optimized for the
single-threaded case: A single pthread_sigmask call instead of several
sigaction system calls.
Therefore it's worth optimizing the single-threaded case here (e.g.
for the 'fatal-signal' module).
The declarative approach that we have offered so far, i.e. some
GNULIB_foo_SINGLE_THREADED declaration in configure.ac, does not work well
because some packages have a few multithreaded and many single-threaded
programs:
- GNU coreutils: sort,
- GNU gettext: msgmerge, msginit.
A declarative approach per program, i.e. allowing the programmer to
declare the single-threaded-ness of a program at the beginning of the
main() function, is not suitable either:
- It would mean another boilerplate call in main() for most programs.
- It would be fragile and unreliable, because a program can be linked
to some libraries that suddenly start to make use of multiple threads.
Therefore, an automatic approach is better. This patch implements that:
It looks whether the program or some of its linked libraries makes use of
a symbol like 'pthread_create' or 'thrd_create'. Excluding libc, of course.
2026-05-16 Bruno Haible <[email protected]>
thread-optim: Add tests.
* tests/test-thread-optim1.c: New file.
* tests/test-thread-optim2.c: New file.
* modules/thread-optim-tests: New file.
thread-optim: Port to older glibc, musl libc, *BSD, Solaris, Android.
* lib/thread-optim.h: Include <stdbool.h>.
(gl_multithreaded): Define differently on ELF platforms.
(_GL_MULTITHREADED_ALWAYS_TRUE, _GL_MULTITHREADED_VIA_ELF): New macros.
(gl_set_multithreaded): New declaration.
* lib/thread-optim.c: New file.
* lib/thread-creators.gperf: New file.
* lib/thread-optim-proto.c: New file.
* modules/thread-optim (Files): Add these files.
(Depends-on): Add gperf, bool, stdint-h.
(configure.ac): Test for <link.h>, dl_iterate_phdr.
(Makefile.am): Compile thread-optim.c. Create thread-creators.h through
gperf.
>From 86488a20eac1fd93282a15e2b9fee0526c080027 Mon Sep 17 00:00:00 2001
From: Bruno Haible <[email protected]>
Date: Sat, 16 May 2026 12:41:01 +0200
Subject: [PATCH 1/2] thread-optim: Port to older glibc, musl libc, *BSD,
Solaris, Android.
* lib/thread-optim.h: Include <stdbool.h>.
(gl_multithreaded): Define differently on ELF platforms.
(_GL_MULTITHREADED_ALWAYS_TRUE, _GL_MULTITHREADED_VIA_ELF): New macros.
(gl_set_multithreaded): New declaration.
* lib/thread-optim.c: New file.
* lib/thread-creators.gperf: New file.
* lib/thread-optim-proto.c: New file.
* modules/thread-optim (Files): Add these files.
(Depends-on): Add gperf, bool, stdint-h.
(configure.ac): Test for <link.h>, dl_iterate_phdr.
(Makefile.am): Compile thread-optim.c. Create thread-creators.h through
gperf.
tweak
---
ChangeLog | 16 ++
lib/thread-creators.gperf | 42 ++++
lib/thread-optim-proto.c | 468 ++++++++++++++++++++++++++++++++++++++
lib/thread-optim.c | 422 ++++++++++++++++++++++++++++++++++
lib/thread-optim.h | 44 ++++
modules/thread-optim | 24 ++
6 files changed, 1016 insertions(+)
create mode 100644 lib/thread-creators.gperf
create mode 100644 lib/thread-optim-proto.c
create mode 100644 lib/thread-optim.c
diff --git a/ChangeLog b/ChangeLog
index 1f2c85cb70..68c125138f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,19 @@
+2026-05-16 Bruno Haible <[email protected]>
+
+ thread-optim: Port to older glibc, musl libc, *BSD, Solaris, Android.
+ * lib/thread-optim.h: Include <stdbool.h>.
+ (gl_multithreaded): Define differently on ELF platforms.
+ (_GL_MULTITHREADED_ALWAYS_TRUE, _GL_MULTITHREADED_VIA_ELF): New macros.
+ (gl_set_multithreaded): New declaration.
+ * lib/thread-optim.c: New file.
+ * lib/thread-creators.gperf: New file.
+ * lib/thread-optim-proto.c: New file.
+ * modules/thread-optim (Files): Add these files.
+ (Depends-on): Add gperf, bool, stdint-h.
+ (configure.ac): Test for <link.h>, dl_iterate_phdr.
+ (Makefile.am): Compile thread-optim.c. Create thread-creators.h through
+ gperf.
+
2026-05-13 Paul Eggert <[email protected]>
intprops-tests: check TYPE_SIGNED (char)
diff --git a/lib/thread-creators.gperf b/lib/thread-creators.gperf
new file mode 100644
index 0000000000..ea90a0c762
--- /dev/null
+++ b/lib/thread-creators.gperf
@@ -0,0 +1,42 @@
+/* List of libc functions that may create threads.
+ Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This file is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation; either version 2.1 of the
+ License, or (at your option) any later version.
+
+ This file is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>. */
+
+%language=ANSI-C
+%define hash-function-name thread_creators_hash
+%define lookup-function-name thread_creators_lookup
+%readonly-tables
+%global-table
+%define word-array-name thread_creators_symbols
+%pic
+%%
+# POSIX API:
+pthread_create
+# The following functions may create a thread when you pass a 'struct sigevent'
+# with the 'sigev_notify field' set to SIGEV_THREAD.
+aio_read
+aio_read64
+aio_write
+aio_write64
+aio_fsync
+aio_fsync64
+lio_listio
+lio_listio64
+timer_create
+mq_notify
+# ISO C11 API:
+thrd_create
+# glibc API:
+getaddrinfo_a
diff --git a/lib/thread-optim-proto.c b/lib/thread-optim-proto.c
new file mode 100644
index 0000000000..2188862e5b
--- /dev/null
+++ b/lib/thread-optim-proto.c
@@ -0,0 +1,468 @@
+/* Prints the PLT/GOT or .dynsym symbols of all attached shared objects.
+
+ Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This file is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation; either version 2.1 of the
+ License, or (at your option) any later version.
+
+ This file is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <[email protected]>, 2026. */
+
+#define _GNU_SOURCE 1
+
+#include <link.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <elf.h>
+
+/* Old versions of <elf.h> lack these types. */
+#define Elf32_Relr Elf32_Word
+#define Elf64_Relr Elf64_Xword
+/* Haiku lacks this macro. */
+#ifndef PF_W
+# define PF_W 0x2
+#endif
+/* NetBSD 9 lacks this macro. */
+#ifndef DT_GNU_HASH
+# define DT_GNU_HASH 0x6ffffef5
+#endif
+/* Many older platforms lack the definitions of these relocations. */
+#ifndef R_X86_64_JUMP_SLOT
+# define R_X86_64_JUMP_SLOT 7
+#endif
+#ifndef R_X86_64_IRELATIVE
+# define R_X86_64_IRELATIVE 37
+#endif
+#ifndef R_386_JMP_SLOT
+# define R_386_JMP_SLOT 7
+#endif
+#ifndef R_AARCH64_JUMP_SLOT
+# define R_AARCH64_JUMP_SLOT 1026
+#endif
+#ifndef R_ARM_JUMP_SLOT
+# define R_ARM_JUMP_SLOT 22
+#endif
+#ifndef R_ALPHA_JMP_SLOT
+# define R_ALPHA_JMP_SLOT 26
+#endif
+#ifndef R_PARISC_IPLT
+# define R_PARISC_IPLT 129
+#endif
+#ifndef R_68K_JMP_SLOT
+# define R_68K_JMP_SLOT 21
+#endif
+#ifndef R_LARCH_JUMP_SLOT
+# define R_LARCH_JUMP_SLOT 5
+#endif
+#ifndef R_PPC_JMP_SLOT
+# define R_PPC_JMP_SLOT 21
+#endif
+#ifndef R_RISCV_JUMP_SLOT
+# define R_RISCV_JUMP_SLOT 5
+#endif
+#ifndef R_390_JMP_SLOT
+# define R_390_JMP_SLOT 11
+#endif
+#ifndef R_SH_JMP_SLOT
+# define R_SH_JMP_SLOT 164
+#endif
+#ifndef R_SPARC_JMP_SLOT
+# define R_SPARC_JMP_SLOT 21
+#endif
+
+/* Definition of __ELF_NATIVE_CLASS. */
+#ifndef __ELF_NATIVE_CLASS
+/* We can't rely on PTRDIFF_WIDTH from <stdint.h> here. */
+# if defined _LP64 || defined __LP64__ \
+ || ((defined __x86_64__ || defined _M_X64) && !(defined __ILP32__ || defined _ILP32)) \
+ || defined __aarch64__ /* arm64 */ \
+ || defined __alpha \
+ || (defined __ia64__ && !defined _ILP32) \
+ || (defined _MIPS_SZLONG && (_MIPS_SZLONG == 64)) /* mips64 */ \
+ || defined __loongarch__ \
+ || defined __powerpc64__ \
+ || (defined __riscv && __riscv_xlen == 64) /* riscv64 */ \
+ || defined __s390x__ \
+ || (defined __sparcv9 || defined __arch64__) /* sparc64 */
+# define __ELF_NATIVE_CLASS 64
+# else
+# define __ELF_NATIVE_CLASS 32
+# endif
+#endif
+
+/* Macros that use __ELF_NATIVE_CLASS. */
+/* Macros for decomposing an r_info field into symbol index and reloc type.
+ Most platforms (except FreeBSD) lack them. */
+#if !(defined ELF_R_SYM && defined ELF_R_TYPE)
+# if __ELF_NATIVE_CLASS == 32
+# define ELF_R_SYM ELF32_R_SYM
+# define ELF_R_TYPE ELF32_R_TYPE
+# elif __ELF_NATIVE_CLASS == 64
+# define ELF_R_SYM ELF64_R_SYM
+# define ELF_R_TYPE ELF64_R_TYPE
+# endif
+#endif
+/* OpenBSD lacks this macro. */
+#ifndef ElfW
+# define ElfW(type) ElfW_1 (__ELF_NATIVE_CLASS, type)
+# define ElfW_1(class,type) ElfW_2 (class, type)
+# define ElfW_2(class,type) Elf ## class ## _ ## type
+#endif
+
+static int
+inspect_one_GOT (struct dl_phdr_info *info, size_t size, void *data)
+{
+ if (info->dlpi_name[0] == '\0')
+ printf ("executable: ");
+ else
+ printf ("library %s: ", info->dlpi_name);
+
+ printf ("size = %zu\n", size);
+
+ ElfW(Addr) base_address = info->dlpi_addr;
+ printf ("base_address = 0x%zx\n", base_address);
+
+ size_t num_headers = info->dlpi_phnum;
+ const ElfW(Phdr) *headers = info->dlpi_phdr;
+ printf ("num_headers = %zu\n", num_headers);
+
+ const ElfW(Dyn) *dynamic_section = NULL;
+ ElfW(Word) dynamic_flags = 0;
+ for (size_t h = 0; h < num_headers; h++)
+ {
+ const ElfW(Phdr) *header = headers + h;
+ if (header->p_type == PT_DYNAMIC)
+ {
+#if 0
+ printf ("Header %zu:\n", h);
+ printf (" type = %zu\n", (size_t) header->p_type);
+ printf (" flags = 0x%zx\n", (size_t) header->p_flags);
+ printf (" offset = 0x%zx\n", (size_t) header->p_offset);
+ printf (" vaddr = 0x%zx\n", (size_t) header->p_vaddr);
+ printf (" paddr = 0x%zx\n", (size_t) header->p_paddr);
+ printf (" filesz = 0x%zx\n", (size_t) header->p_filesz);
+ printf (" memsz = 0x%zx\n", (size_t) header->p_memsz);
+ printf (" align = 0x%zx\n", (size_t) header->p_align);
+#endif
+ dynamic_section = (const ElfW(Dyn) *) (base_address + header->p_vaddr);
+ dynamic_flags = header->p_flags;
+ break;
+ }
+ }
+ printf ("Dynamic section = 0x%zx\n", (ElfW(Addr)) dynamic_section);
+ if (dynamic_section != NULL)
+ {
+ ElfW(Addr) maybe_base_address;
+#if __GLIBC__ >= 2 && !(defined __mips__ || defined __riscv)
+ /* linux-vdso.so.1 is weird. */
+ if (dynamic_flags & PF_W)
+ maybe_base_address = 0;
+ else
+#endif
+ maybe_base_address = base_address;
+
+ /* Get the string table. */
+ const char *strtab = NULL;
+ size_t strsz = 0;
+ for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+ dynamic_section_entry->d_tag != DT_NULL;
+ dynamic_section_entry++)
+ {
+ if (dynamic_section_entry->d_tag == DT_STRTAB)
+ strtab = (const char *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+ if (dynamic_section_entry->d_tag == DT_STRSZ)
+ strsz = dynamic_section_entry->d_un.d_val;
+ }
+
+ /* Get the symbol table. */
+ const ElfW(Sym) *symtab = NULL;
+ size_t symentsz = 0;
+ const unsigned int *hash = NULL;
+ const unsigned int *gnu_hash = NULL;
+ for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+ dynamic_section_entry->d_tag != DT_NULL;
+ dynamic_section_entry++)
+ {
+ if (dynamic_section_entry->d_tag == DT_SYMTAB)
+ symtab = (const ElfW(Sym) *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+ if (dynamic_section_entry->d_tag == DT_SYMENT)
+ symentsz = dynamic_section_entry->d_un.d_val;
+ if (dynamic_section_entry->d_tag == DT_HASH)
+ hash = (const unsigned int *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+ if (dynamic_section_entry->d_tag == DT_GNU_HASH)
+ gnu_hash = (const unsigned int *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+ }
+ printf ("strtab = 0x%zx, total size strsz = %zu\n", (ElfW(Addr)) strtab, strsz);
+ printf ("symtab = 0x%zx, each element of size symentsz = %zu\n", (ElfW(Addr)) symtab, symentsz);
+ /* Check the symentsz. */
+ if (!(symentsz == sizeof (ElfW(Sym))))
+ /* Invalid symentsz.
+ Should not happen with properly formed ELF files. */
+ abort ();
+ /* How many symbols in symtab?
+ The old method uses DT_HASH, newer binaries use DT_GNU_HASH. */
+ size_t num_symtab_entries = (size_t)-1;
+ if (hash != NULL)
+ {
+ /* The DT_HASH pointer points to
+ struct elf_hash_table
+ {
+ uint32_t nbucket;
+ uint32_t nchain;
+ uint32_t bucket[nbucket];
+ uint32_t chain[nchain];
+ }
+ where nchain is the number of entries in the symbol table.
+ See <https://flapenguin.me/elf-dt-hash>. */
+ num_symtab_entries = hash[1];
+ printf ("num_symtab_entries according to HASH = %zu\n", num_symtab_entries);
+ /* On Linux/s390x this value is too small. The one from GNU_HASH is correct. */
+ }
+ if (gnu_hash != NULL)
+ {
+ /* The DT_GNU_HASH pointer points to
+ struct gnu_hash_table
+ {
+ uint32_t nbuckets;
+ uint32_t symoffset;
+ uint32_t bloom_size;
+ uint32_t bloom_shift;
+ ElfW(Word) bloom[bloom_size];
+ uint32_t buckets[nbuckets];
+ uint32_t chain[];
+ }
+ See <https://flapenguin.me/elf-dt-gnu-hash> and
+ binutils/bfd/elflink.c:bfd_elf_size_dynsym_hash_dynstr(). */
+ unsigned int nbuckets = gnu_hash[0];
+ unsigned int symoffset = gnu_hash[1];
+ unsigned int bloom_size = gnu_hash[2];
+ const unsigned int *buckets_start = &gnu_hash[4 + bloom_size * (__ELF_NATIVE_CLASS / 32)];
+ size_t gnu_num_symtab_entries;
+ if (nbuckets == 1 && symoffset == 1 && bloom_size == 1 && gnu_hash[3] == 0
+ && buckets_start[0] == 0)
+ gnu_num_symtab_entries = 0;
+ else
+ {
+ const unsigned int *chain_start = &buckets_start[nbuckets];
+ const unsigned int *chain_p = chain_start;
+ for (unsigned int bucket = 0; bucket < nbuckets; bucket++)
+ if (buckets_start[bucket] != 0)
+ {
+ while ((*chain_p & 1) == 0)
+ chain_p++;
+ chain_p++;
+ }
+ gnu_num_symtab_entries = symoffset + (chain_p - chain_start);
+ }
+ printf ("num_symtab_entries according to GNU_HASH = %zu\n", gnu_num_symtab_entries);
+ if (gnu_num_symtab_entries != 0)
+ num_symtab_entries = gnu_num_symtab_entries;
+ }
+
+ /* List the symbols in the .dynsym section.
+ $ readelf -a -X a.out */
+ if (num_symtab_entries > 0 && num_symtab_entries < (size_t)-1)
+ {
+ printf ("Symbols in the .dynsym section:\n");
+ for (size_t symbol_index = 1; symbol_index < num_symtab_entries; symbol_index++)
+ {
+ const ElfW(Sym) *symtab_entry = symtab + symbol_index;
+ if (symtab_entry->st_shndx == SHN_UNDEF)
+ {
+ ElfW(Word) symbol_name_offset = symtab_entry->st_name;
+ const char *symbol_name = strtab + symbol_name_offset;
+ printf ("Symtab[%zu]: %s\n", symbol_index, symbol_name);
+ }
+ }
+ }
+
+#if defined __mips__ /* MIPS does not have the usual GOT structure */
+
+#if 0 /* works but not useful */
+
+ const void *got_entries = NULL;
+ size_t num_got_entries = 0;
+ for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+ dynamic_section_entry->d_tag != DT_NULL;
+ dynamic_section_entry++)
+ {
+ if (dynamic_section_entry->d_tag == DT_PLTGOT)
+ {
+ printf ("PLTGOT value: 0x%zx\n", dynamic_section_entry->d_un.d_ptr);
+ got_entries = (const void *) dynamic_section_entry->d_un.d_ptr;
+ }
+ if (dynamic_section_entry->d_tag == DT_MIPS_LOCAL_GOTNO)
+ {
+ printf ("DT_MIPS_LOCAL_GOTNO value: 0x%zx\n", dynamic_section_entry->d_un.d_val);
+ num_got_entries = dynamic_section_entry->d_un.d_val;
+ }
+ }
+
+#endif
+
+#else
+
+ int jump_relocation_type = 0;
+ const void *jump_relocations = NULL;
+ size_t jump_relocations_size = 0;
+ for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+ dynamic_section_entry->d_tag != DT_NULL;
+ dynamic_section_entry++)
+ {
+ if (dynamic_section_entry->d_tag == DT_PLTREL)
+ {
+ printf ("PLTREL value: 0x%zx", dynamic_section_entry->d_un.d_val);
+#if defined __x86_64__ || defined __aarch64__ || defined __alpha || defined __hppa || defined __m68k__ || defined _ARCH_PPC || defined __riscv || defined __s390__ || defined __sh__ || defined __sparc
+ if (dynamic_section_entry->d_un.d_val == DT_RELA)
+ printf (" = DT_RELA");
+#elif defined __i386__ || defined __arm__
+ if (dynamic_section_entry->d_un.d_val == DT_REL)
+ printf (" = DT_REL");
+#endif
+ jump_relocation_type = dynamic_section_entry->d_un.d_val;
+ printf ("\n");
+ }
+ else if (dynamic_section_entry->d_tag == DT_PLTRELSZ)
+ {
+ printf ("PLTRELSZ value: 0x%zx\n", dynamic_section_entry->d_un.d_val);
+ jump_relocations_size = dynamic_section_entry->d_un.d_val;
+ }
+ else if (dynamic_section_entry->d_tag == DT_JMPREL)
+ {
+ printf ("JMPREL value: 0x%zx\n", dynamic_section_entry->d_un.d_ptr);
+#if __GLIBC__ >= 2 && !defined __riscv
+ jump_relocations = (const void *) dynamic_section_entry->d_un.d_ptr;
+#else /* glibc/riscv, musl libc, FreeBSD, NetBSD, OpenBSD, Solaris, Haiku, Android */
+ jump_relocations = (const void *) (base_address + dynamic_section_entry->d_un.d_ptr);
+#endif
+ }
+ else if (dynamic_section_entry->d_tag == DT_PLTGOT)
+ {
+ /* This points to the GOT. It contains the real function addresses,
+ but no symbolic information. */
+ printf ("PLTGOT value: 0x%zx\n", dynamic_section_entry->d_un.d_ptr);
+ }
+ }
+ if (jump_relocations != NULL)
+ {
+ if (!(jump_relocation_type == DT_REL || jump_relocation_type == DT_RELA))
+ /* No valid jump_relocation_type.
+ Should not happen with properly formed ELF files. */
+ abort ();
+
+ size_t num_jump_relocations =
+ jump_relocations_size
+ / (jump_relocation_type == DT_RELA ? sizeof (ElfW(Rela)) : sizeof (ElfW(Rel)));
+
+ for (size_t j = 0; j < num_jump_relocations; j++)
+ {
+ ElfW(Addr) got_element;
+ ElfW(Relr) r_info;
+ if (jump_relocation_type == DT_RELA)
+ {
+ const ElfW(Rela) *jump_reloc = (const ElfW(Rela) *) jump_relocations + j;
+ got_element = base_address + jump_reloc->r_offset;
+ r_info = jump_reloc->r_info;
+ }
+ else /* jump_relocation_type == DT_REL */
+ {
+ const ElfW(Rel) *jump_reloc = (const ElfW(Rel) *) jump_relocations + j;
+ got_element = base_address + jump_reloc->r_offset;
+ r_info = jump_reloc->r_info;
+ }
+ unsigned int symbol_index = ELF_R_SYM (r_info);
+ unsigned int reloc_type = ELF_R_TYPE (r_info);
+ (void) reloc_type;
+ /* We can ignore relocations not connected to a symbol. */
+ if (symbol_index != 0)
+ {
+ if (!(symbol_index < num_symtab_entries))
+ /* Out-of-range reference to the symtab.
+ Should not happen with properly formed ELF files. */
+ abort ();
+ const ElfW(Sym) *symtab_entry = symtab + symbol_index;
+ ElfW(Word) symbol_name_offset = symtab_entry->st_name;
+ if (!(symbol_name_offset < strsz))
+ /* Out-of-range reference to the strtab.
+ Should not happen with properly formed ELF files. */
+ abort ();
+ const char *symbol_name = strtab + symbol_name_offset;
+ printf ("Jump reloc: address = 0x%zx, reloc_type = 0x%x",
+ got_element, reloc_type);
+#if defined __x86_64__
+ if (reloc_type == R_X86_64_JUMP_SLOT)
+ printf (" = R_X86_64_JUMP_SLOT");
+ else if (reloc_type == R_X86_64_IRELATIVE)
+ printf (" = R_X86_64_IRELATIVE");
+#elif defined __i386__
+ if (reloc_type == R_386_JMP_SLOT)
+ printf (" = R_386_JMP_SLOT");
+#elif defined __aarch64__
+ if (reloc_type == R_AARCH64_JUMP_SLOT)
+ printf (" = R_AARCH64_JUMP_SLOT");
+#elif defined __arm__
+ if (reloc_type == R_ARM_JUMP_SLOT)
+ printf (" = R_ARM_JUMP_SLOT");
+#elif defined __alpha
+ if (reloc_type == R_ALPHA_JMP_SLOT)
+ printf (" = R_ALPHA_JMP_SLOT");
+#elif defined __hppa
+ if (reloc_type == R_PARISC_IPLT)
+ printf (" = R_PARISC_IPLT");
+#elif defined __m68k__
+ if (reloc_type == R_68K_JMP_SLOT)
+ printf (" = R_68K_JMP_SLOT");
+#elif defined __loongarch__
+ if (reloc_type == R_LARCH_JUMP_SLOT)
+ printf (" = R_LARCH_JUMP_SLOT");
+#elif defined _ARCH_PPC
+ if (reloc_type == R_PPC_JMP_SLOT)
+ printf (" = R_PPC_JMP_SLOT");
+#elif defined __riscv
+ if (reloc_type == R_RISCV_JUMP_SLOT)
+ printf (" = R_RISCV_JUMP_SLOT");
+#elif defined __s390__
+ if (reloc_type == R_390_JMP_SLOT)
+ printf (" = R_390_JMP_SLOT");
+#elif defined __sh__
+ if (reloc_type == R_SH_JMP_SLOT)
+ printf (" = R_SH_JMP_SLOT");
+#elif defined __sparc
+ if (reloc_type == R_SPARC_JMP_SLOT)
+ printf (" = R_SPARC_JMP_SLOT");
+#endif
+ printf (", symbol_index = 0x%x = %s",
+ symbol_index, symbol_name);
+ printf ("\n");
+ }
+ }
+ }
+
+#endif
+ }
+ printf ("\n");
+
+ return 0;
+}
+
+int
+main ()
+{
+ /* For debugging: Prevent fully buffered mode of stdout. */
+ setvbuf (stdout, NULL, _IOLBF, 0);
+
+ dl_iterate_phdr (inspect_one_GOT, NULL);
+
+ return 0;
+}
diff --git a/lib/thread-optim.c b/lib/thread-optim.c
new file mode 100644
index 0000000000..88812307ed
--- /dev/null
+++ b/lib/thread-optim.c
@@ -0,0 +1,422 @@
+/* Optimization of multithreaded code.
+
+ Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This file is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License as
+ published by the Free Software Foundation; either version 2.1 of the
+ License, or (at your option) any later version.
+
+ This file is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <[email protected]>, 2026. */
+
+#include <config.h>
+
+/* Specification. */
+#include "thread-optim.h"
+
+static int mt_override = -1;
+
+void
+gl_set_multithreaded (bool mt)
+{
+ mt_override = (mt ? 1 : 0);
+}
+
+#ifdef _GL_MULTITHREADED_VIA_ELF
+
+/* Three possible approaches come to mind for automatically determining
+ whether a process is single-threaded:
+
+ * Look at the number of entries in the /proc/self/task/ directory.
+ If there is only one, the process is (currently) single-threaded.
+ Drawbacks:
+ - It works only on Linux.
+ - It is relatively expensive (several system calls).
+
+ * Look at the state of the PLT and GOT entries for the functions
+ 'pthread_create' and 'thrd_create'. If they are both still in the
+ initial state, neither of them has been called so far, and the
+ process therefore is (currently) single-threaded.
+ Drawbacks:
+ - It does not work on modern Linux distros, because they link
+ with options '-z now -z relro' ('-z relro' for security
+ reasons, and '-z now' because it it required for '-z now')
+ and '-z now' activates early binding instead of lazy binding
+ in the PLT and GOT.
+ - The code for doing this is architecture dependent (because
+ the instructions in the PLT are obviously arch dependent)
+ and OS dependent (because OpenBSD has a different structure
+ for the PLT than the other OSes). Additionally it requires
+ inline assembly in order to get the value of the
+ 'pthread_create@plt' and 'thrd_create@plt' symbols.
+
+ * Look at whether the symbols 'pthread_create' and 'thrd_create'
+ occur in the PLT and GOT. If neither of them occurs (in the
+ executable and in the linked shared libraries), the process
+ cannot create threads.
+ Drawbacks:
+ - This approach works only under the following assumptions:
+ . These two entry points are the only facilities in libc
+ that create threads.
+ . The process will not invoke dlopen() to attach other
+ shared libraries.
+ . The program is not statically linked.
+
+ Here we implement the third approach. We mitigate the drawback
+ by allowing the programmer to override the result if the assumptions
+ are not met. */
+
+#include <link.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <elf.h>
+
+/* Old versions of <elf.h> lack these types. */
+#define Elf32_Relr Elf32_Word
+#define Elf64_Relr Elf64_Xword
+/* Haiku lacks this macro. */
+#ifndef PF_W
+# define PF_W 0x2
+#endif
+/* NetBSD 9 lacks this macro. */
+#ifndef DT_GNU_HASH
+# define DT_GNU_HASH 0x6ffffef5
+#endif
+
+/* Definition of __ELF_NATIVE_CLASS. */
+#ifndef __ELF_NATIVE_CLASS
+# if PTRDIFF_WIDTH == 64
+# define __ELF_NATIVE_CLASS 64
+# else
+# define __ELF_NATIVE_CLASS 32
+# endif
+#endif
+
+/* Macros that use __ELF_NATIVE_CLASS. */
+/* Macros for decomposing an r_info field into symbol index and reloc type.
+ Most platforms (except FreeBSD) lack them. */
+#if !(defined ELF_R_SYM && defined ELF_R_TYPE)
+# if __ELF_NATIVE_CLASS == 32
+# define ELF_R_SYM ELF32_R_SYM
+# define ELF_R_TYPE ELF32_R_TYPE
+# elif __ELF_NATIVE_CLASS == 64
+# define ELF_R_SYM ELF64_R_SYM
+# define ELF_R_TYPE ELF64_R_TYPE
+# endif
+#endif
+/* OpenBSD lacks this macro. */
+#ifndef ElfW
+# define ElfW(type) ElfW_1 (__ELF_NATIVE_CLASS, type)
+# define ElfW_1(class,type) ElfW_2 (class, type)
+# define ElfW_2(class,type) Elf ## class ## _ ## type
+#endif
+
+#include "thread-creators.h"
+
+/* We inspect the GOT. This is more robust than inspecting the PLT, because
+ - When the executable is built with option '-fno-plt', there is no PLT,
+ only a GOT.
+ - The ELF data structures accessible from dl_iterate_phdr() don't
+ actually contain a pointer to the PLT. In order to recognize the PLT,
+ we would have to look for architecture specific relocations. */
+
+/* Inspects the GOT of a single ELF file in memory.
+ Returns 1 if a thread creator symbol is seen,
+ -1 if the executable is statically linked, or
+ 0 otherwise. */
+static int
+inspect_one_GOT (struct dl_phdr_info *info, size_t size, void *data)
+{
+ /* IMPORTANT: Before changing this code, make the corresponding changes
+ to thread-optim-proto.c and test them. */
+
+ /* Skip the processing of libc.so.
+ * On specific platforms, this is necessary:
+ - FreeBSD 12/arm64 has lio_listio in the GOT of libc.so.
+ - NetBSD 10 has timer_create in the GOT of libc.so.
+ - Android has pthread_create in the GOT of libc.so.
+ This does not make the process multithreaded.
+ * On the other platforms, it is just an optimization, to not scan
+ the (possibly many) symbols of libc.so. */
+ {
+ const char *filename = info->dlpi_name;
+ const char *last_slash = strrchr (filename, '/');
+ if (last_slash != NULL
+ && strncmp (last_slash + 1, "libc.so", 7) == 0
+ && (last_slash[1 + 7] == '\0' || last_slash[1 + 7] == '.'))
+ return 0;
+ }
+
+ ElfW(Addr) base_address = info->dlpi_addr;
+
+ size_t num_headers = info->dlpi_phnum;
+ const ElfW(Phdr) *headers = info->dlpi_phdr;
+
+ const ElfW(Dyn) *dynamic_section = NULL;
+ ElfW(Word) dynamic_flags _GL_ATTRIBUTE_MAYBE_UNUSED = 0;
+ for (size_t h = 0; h < num_headers; h++)
+ {
+ const ElfW(Phdr) *header = headers + h;
+ if (header->p_type == PT_DYNAMIC)
+ {
+ dynamic_section = (const ElfW(Dyn) *) (base_address + header->p_vaddr);
+ dynamic_flags = header->p_flags;
+ break;
+ }
+ }
+ if (dynamic_section != NULL)
+ {
+ ElfW(Addr) maybe_base_address;
+#if __GLIBC__ >= 2 && !(defined __mips__ || defined __riscv)
+ /* linux-vdso.so.1 is weird. */
+ if (dynamic_flags & PF_W)
+ maybe_base_address = 0;
+ else
+#endif
+ maybe_base_address = base_address;
+
+ /* Get the string table. */
+ const char *strtab = NULL;
+ size_t strsz = 0;
+ for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+ dynamic_section_entry->d_tag != DT_NULL;
+ dynamic_section_entry++)
+ {
+ if (dynamic_section_entry->d_tag == DT_STRTAB)
+ strtab = (const char *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+ if (dynamic_section_entry->d_tag == DT_STRSZ)
+ strsz = dynamic_section_entry->d_un.d_val;
+ }
+
+ /* Get the symbol table. */
+ const ElfW(Sym) *symtab = NULL;
+ size_t symentsz = 0;
+ const unsigned int *hash = NULL;
+ const unsigned int *gnu_hash = NULL;
+ for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+ dynamic_section_entry->d_tag != DT_NULL;
+ dynamic_section_entry++)
+ {
+ if (dynamic_section_entry->d_tag == DT_SYMTAB)
+ symtab = (const ElfW(Sym) *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+ if (dynamic_section_entry->d_tag == DT_SYMENT)
+ symentsz = dynamic_section_entry->d_un.d_val;
+ if (dynamic_section_entry->d_tag == DT_HASH)
+ hash = (const unsigned int *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+ if (dynamic_section_entry->d_tag == DT_GNU_HASH)
+ gnu_hash = (const unsigned int *) (maybe_base_address + dynamic_section_entry->d_un.d_ptr);
+ }
+ if (!(symentsz == sizeof (ElfW(Sym))))
+ /* Invalid symentsz.
+ Should not happen with properly formed ELF files. */
+ abort ();
+ size_t num_symtab_entries = (size_t)-1;
+ if (hash != NULL)
+ {
+ /* The DT_HASH pointer points to
+ struct elf_hash_table
+ {
+ uint32_t nbucket;
+ uint32_t nchain;
+ uint32_t bucket[nbucket];
+ uint32_t chain[nchain];
+ }
+ where nchain is the number of entries in the symbol table.
+ See <https://flapenguin.me/elf-dt-hash>. */
+ num_symtab_entries = hash[1];
+ }
+ if (gnu_hash != NULL)
+ {
+ /* The DT_GNU_HASH pointer points to
+ struct gnu_hash_table
+ {
+ uint32_t nbuckets;
+ uint32_t symoffset;
+ uint32_t bloom_size;
+ uint32_t bloom_shift;
+ ElfW(Word) bloom[bloom_size];
+ uint32_t buckets[nbuckets];
+ uint32_t chain[];
+ }
+ See <https://flapenguin.me/elf-dt-gnu-hash> and
+ binutils/bfd/elflink.c:bfd_elf_size_dynsym_hash_dynstr(). */
+ unsigned int nbuckets = gnu_hash[0];
+ unsigned int symoffset = gnu_hash[1];
+ unsigned int bloom_size = gnu_hash[2];
+ const unsigned int *buckets_start = &gnu_hash[4 + bloom_size * (__ELF_NATIVE_CLASS / 32)];
+ size_t gnu_num_symtab_entries;
+ if (nbuckets == 1 && symoffset == 1 && bloom_size == 1 && gnu_hash[3] == 0
+ && buckets_start[0] == 0)
+ gnu_num_symtab_entries = 0;
+ else
+ {
+ const unsigned int *chain_start = &buckets_start[nbuckets];
+ const unsigned int *chain_p = chain_start;
+ for (unsigned int bucket = 0; bucket < nbuckets; bucket++)
+ if (buckets_start[bucket] != 0)
+ {
+ while ((*chain_p & 1) == 0)
+ chain_p++;
+ chain_p++;
+ }
+ gnu_num_symtab_entries = symoffset + (chain_p - chain_start);
+ }
+ if (gnu_num_symtab_entries != 0)
+ num_symtab_entries = gnu_num_symtab_entries;
+ }
+
+# if defined __mips__
+
+ /* MIPS does not have the usual GOT structure.
+ Instead, inspect the symbols in the .dynsym section. This happens
+ to be the same set of symbols as the symbols in the symtab marked as
+ UNDEF. This is a somewhat larger set of symbols, but still good
+ enough. */
+ for (size_t symbol_index = 1; symbol_index < num_symtab_entries; symbol_index++)
+ {
+ const ElfW(Sym) *symtab_entry = symtab + symbol_index;
+ if (symtab_entry->st_shndx == SHN_UNDEF)
+ {
+ ElfW(Word) symbol_name_offset = symtab_entry->st_name;
+ const char *symbol_name = strtab + symbol_name_offset;
+ if (thread_creators_lookup (symbol_name, strlen (symbol_name)) != NULL)
+ /* Found a jump relocation to a thread creator symbol. */
+ return 1;
+ }
+ }
+
+#else
+
+ int jump_relocation_type = 0;
+ const void *jump_relocations = NULL;
+ size_t jump_relocations_size = 0;
+ for (const ElfW(Dyn) *dynamic_section_entry = dynamic_section;
+ dynamic_section_entry->d_tag != DT_NULL;
+ dynamic_section_entry++)
+ {
+ if (dynamic_section_entry->d_tag == DT_PLTREL)
+ {
+ /* The value should be DT_REL or DT_RELA. */
+ jump_relocation_type = dynamic_section_entry->d_un.d_val;
+ }
+ else if (dynamic_section_entry->d_tag == DT_PLTRELSZ)
+ {
+ jump_relocations_size = dynamic_section_entry->d_un.d_val;
+ }
+ else if (dynamic_section_entry->d_tag == DT_JMPREL)
+ {
+# if __GLIBC__ >= 2 && !defined __riscv
+ jump_relocations = (const void *) dynamic_section_entry->d_un.d_ptr;
+# else /* glibc/riscv, musl libc, FreeBSD, NetBSD, OpenBSD, Solaris, Haiku, Android */
+ jump_relocations = (const void *) (base_address + dynamic_section_entry->d_un.d_ptr);
+# endif
+ }
+ /* The entry with tag DT_PLTGOT contains a pointer to the GOT
+ in memory. But we don't need it actually. */
+ }
+ if (jump_relocations != NULL)
+ {
+ if (!(jump_relocation_type == DT_REL || jump_relocation_type == DT_RELA))
+ /* No valid jump_relocation_type.
+ Should not happen with properly formed ELF files. */
+ abort ();
+
+ size_t num_jump_relocations =
+ jump_relocations_size
+ / (jump_relocation_type == DT_RELA ? sizeof (ElfW(Rela)) : sizeof (ElfW(Rel)));
+
+ for (size_t j = 0; j < num_jump_relocations; j++)
+ {
+ ElfW(Addr) got_element _GL_ATTRIBUTE_MAYBE_UNUSED;
+ ElfW(Relr) r_info;
+ if (jump_relocation_type == DT_RELA)
+ {
+ const ElfW(Rela) *jump_reloc = (const ElfW(Rela) *) jump_relocations + j;
+ got_element = base_address + jump_reloc->r_offset;
+ r_info = jump_reloc->r_info;
+ }
+ else /* jump_relocation_type == DT_REL */
+ {
+ const ElfW(Rel) *jump_reloc = (const ElfW(Rel) *) jump_relocations + j;
+ got_element = base_address + jump_reloc->r_offset;
+ r_info = jump_reloc->r_info;
+ }
+ unsigned int symbol_index = ELF_R_SYM (r_info);
+ unsigned int reloc_type = ELF_R_TYPE (r_info);
+ (void) reloc_type;
+ /* We can ignore relocations not connected to a symbol. */
+ if (symbol_index != 0)
+ {
+ if (!(symbol_index < num_symtab_entries))
+ /* Out-of-range reference to the symtab.
+ Should not happen with properly formed ELF files. */
+ abort ();
+ const ElfW(Sym) *symtab_entry = symtab + symbol_index;
+ ElfW(Word) symbol_name_offset = symtab_entry->st_name;
+ if (!(symbol_name_offset < strsz))
+ /* Out-of-range reference to the strtab.
+ Should not happen with properly formed ELF files. */
+ abort ();
+ const char *symbol_name = strtab + symbol_name_offset;
+ if (thread_creators_lookup (symbol_name, strlen (symbol_name)) != NULL)
+ /* Found a jump relocation to a thread creator symbol. */
+ return 1;
+ }
+ }
+ }
+
+#endif
+ }
+ else
+ {
+ if (info->dlpi_name[0] == '\0')
+ /* The executable is statically linked. */
+ return -1;
+ }
+
+ return 0;
+}
+
+/* Returns true if the process is possibly multithreaded throughout its
+ lifetime (assuming the process will not invoke dlopen() to attach other
+ shared libraries). */
+static bool
+is_multithreaded_uncached (void)
+{
+ int result = dl_iterate_phdr (inspect_one_GOT, NULL);
+ /* result is 1 if a thread creator symbol is seen in some of the GOTs,
+ -1 if the executable is statically linked, or
+ 0 otherwise. */
+ return result != 0;
+}
+
+bool
+gl_multithreaded (void)
+{
+ /* Consider the override. */
+ if (mt_override >= 0)
+ return mt_override;
+ else
+ {
+ /* Cache the result from is_multithreaded_uncached. */
+ static int volatile cached_result = -1;
+
+ if (cached_result < 0)
+ cached_result = is_multithreaded_uncached ();
+
+ return cached_result;
+ }
+}
+
+#endif /* _GL_MULTITHREADED_VIA_ELF */
diff --git a/lib/thread-optim.h b/lib/thread-optim.h
index 104fd90fc9..e4c0e03f5e 100644
--- a/lib/thread-optim.h
+++ b/lib/thread-optim.h
@@ -55,11 +55,55 @@
#error "Please include config.h first."
#endif
+#include <stdbool.h>
+
#if HAVE_SYS_SINGLE_THREADED_H /* glibc >= 2.32 */
+
# include <sys/single_threaded.h>
# define gl_multithreaded() (!__libc_single_threaded)
+
+#elif defined __APPLE__ && defined __MACH__ /* macOS */
+
+/* One of the most basic libraries on macOS, CoreFoundation,
+ creates new threads without being asked for. Therefore,
+ assume that the process might be multithreaded. */
+# define gl_multithreaded() 1
+# define _GL_MULTITHREADED_ALWAYS_TRUE
+
+#elif defined __ELF__ /* Linux, Hurd, FreeBSD, NetBSD, OpenBSD, Solaris, ... */\
+ && HAVE_LINK_H \
+ && HAVE_DL_ITERATE_PHDR /* not Solaris 10, not Haiku */
+
+/* We can detect whether pthread_create() can be invoked, by looking
+ at the contents of the PLT of the executable and of each shared object. */
+# ifdef __cplusplus
+extern "C" {
+# endif
+extern bool gl_multithreaded (void);
+# define _GL_MULTITHREADED_VIA_ELF
+# ifdef __cplusplus
+}
+# endif
+
#else
+
+/* On other platforms, such as Windows, threads are created for relatively
+ simple tasks, such as events, alarms, or interruptible sleep. Therefore,
+ assume that the process might be multithreaded. */
# define gl_multithreaded() 1
+# define _GL_MULTITHREADED_ALWAYS_TRUE
+
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Allows programs to override the result of gl_multithreaded(). */
+extern void gl_set_multithreaded (bool mt);
+
+#ifdef __cplusplus
+}
#endif
#endif /* _THREAD_OPTIM_H */
diff --git a/modules/thread-optim b/modules/thread-optim
index caf45189c4..5acbbe5855 100644
--- a/modules/thread-optim
+++ b/modules/thread-optim
@@ -3,13 +3,37 @@ Optimization of multithreaded code.
Files:
lib/thread-optim.h
+lib/thread-optim.c
+lib/thread-creators.gperf
+lib/thread-optim-proto.c
Depends-on:
+gperf
+bool
+stdint-h
configure.ac:
AC_CHECK_HEADERS([sys/single_threaded.h])
+AC_CHECK_HEADERS([link.h])
+dnl Solaris 10 has dl_iterate_phdr in a separate library libdl.
+dnl Haiku has dl_iterate_phdr in a separate library libbsd.
+dnl We don't want a link dependency here.
+AC_CHECK_FUNCS([dl_iterate_phdr])
Makefile.am:
+lib_SOURCES += thread-optim.c
+
+$(srcdir)/thread-creators.h: $(srcdir)/thread-creators.gperf
+ $(V_GPERF)$(GPERF) -m 10 $(srcdir)/thread-creators.gperf > $(srcdir)/thread-creators.h-t1 \
+ && sed -e 's/^const/static const/' \
+ -e 's|\([ "]\)[^ "]*/thread-creators\.gperf\([ "]\)|\1thread-creators.gperf\2|' \
+ < $(srcdir)/thread-creators.h-t1 > $(srcdir)/thread-creators.h-t2 \
+ && rm -f $(srcdir)/thread-creators.h-t1 \
+ && mv $(srcdir)/thread-creators.h-t2 $(srcdir)/thread-creators.h
+BUILT_SOURCES += thread-creators.h
+MOSTLYCLEANFILES += thread-creators.h-t1 thread-creators.h-t2
+MAINTAINERCLEANFILES += thread-creators.h
+EXTRA_DIST += thread-creators.h
Include:
"thread-optim.h"
--
2.54.0
>From d9e15740b500990c96623285a8c26a92e0c6da0f Mon Sep 17 00:00:00 2001
From: Bruno Haible <[email protected]>
Date: Sat, 16 May 2026 12:42:39 +0200
Subject: [PATCH 2/2] thread-optim: Add tests.
* tests/test-thread-optim1.c: New file.
* tests/test-thread-optim2.c: New file.
* modules/thread-optim-tests: New file.
---
ChangeLog | 5 ++++
modules/thread-optim-tests | 14 ++++++++++
tests/test-thread-optim1.c | 36 ++++++++++++++++++++++++++
tests/test-thread-optim2.c | 52 ++++++++++++++++++++++++++++++++++++++
4 files changed, 107 insertions(+)
create mode 100644 modules/thread-optim-tests
create mode 100644 tests/test-thread-optim1.c
create mode 100644 tests/test-thread-optim2.c
diff --git a/ChangeLog b/ChangeLog
index 68c125138f..c51d480e87 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,10 @@
2026-05-16 Bruno Haible <[email protected]>
+ thread-optim: Add tests.
+ * tests/test-thread-optim1.c: New file.
+ * tests/test-thread-optim2.c: New file.
+ * modules/thread-optim-tests: New file.
+
thread-optim: Port to older glibc, musl libc, *BSD, Solaris, Android.
* lib/thread-optim.h: Include <stdbool.h>.
(gl_multithreaded): Define differently on ELF platforms.
diff --git a/modules/thread-optim-tests b/modules/thread-optim-tests
new file mode 100644
index 0000000000..0de5bfa8b3
--- /dev/null
+++ b/modules/thread-optim-tests
@@ -0,0 +1,14 @@
+Files:
+tests/test-thread-optim1.c
+tests/test-thread-optim2.c
+tests/macros.h
+
+Depends-on:
+thread
+
+configure.ac:
+
+Makefile.am:
+TESTS += test-thread-optim1 test-thread-optim2
+check_PROGRAMS += test-thread-optim1 test-thread-optim2
+test_thread_optim2_LDADD = $(LDADD) @LIBMULTITHREAD@
diff --git a/tests/test-thread-optim1.c b/tests/test-thread-optim1.c
new file mode 100644
index 0000000000..3fc33a8ec2
--- /dev/null
+++ b/tests/test-thread-optim1.c
@@ -0,0 +1,36 @@
+/* Test of optimization of multithreaded code..
+ Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <[email protected]>, 2026. */
+
+#include <config.h>
+
+/* Specification. */
+#include "thread-optim.h"
+
+#include "macros.h"
+
+int
+main ()
+{
+#ifdef _GL_MULTITHREADED_ALWAYS_TRUE
+ ASSERT (gl_multithreaded ());
+#else
+ ASSERT (! gl_multithreaded ());
+#endif
+
+ return test_exit_status;
+}
diff --git a/tests/test-thread-optim2.c b/tests/test-thread-optim2.c
new file mode 100644
index 0000000000..bd2e3b2955
--- /dev/null
+++ b/tests/test-thread-optim2.c
@@ -0,0 +1,52 @@
+/* Test of optimization of multithreaded code..
+ Copyright (C) 2026 Free Software Foundation, Inc.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>. */
+
+/* Written by Bruno Haible <[email protected]>, 2026. */
+
+#include <config.h>
+
+/* Specification. */
+#include "thread-optim.h"
+
+#include "glthread/thread.h"
+
+#include "macros.h"
+
+static int dummy;
+
+static void *
+thread_func (void *arg)
+{
+ return arg;
+}
+
+int
+main ()
+{
+#if !defined __gnu_hurd__
+# if defined _GL_MULTITHREADED_ALWAYS_TRUE || defined _GL_MULTITHREADED_VIA_ELF
+ ASSERT (gl_multithreaded ());
+# else
+ ASSERT (! gl_multithreaded ());
+# endif
+#endif
+
+ (void) gl_thread_create (thread_func, &dummy);
+
+ ASSERT (gl_multithreaded ());
+
+ return test_exit_status;
+}
--
2.54.0