Repository navigation
Windows: import hashlib loads user32.dll and the GDI DLLs through libcrypto; delay-load user32 #159008
Description
Description
On Windows, importing hashlib maps user32.dll, gdi32.dll, gdi32full.dll, win32u.dll and
imm32.dll into the process. So do hmac, secrets, ssl, asyncio, urllib.request,
http.client, and therefore pip and most network code. CPython does not use any of them.
They come from libcrypto-3.dll, which _hashlib and _ssl link. Its import table names three
user32 functions:
GetProcessWindowStationandGetUserObjectInformationW, called byOPENSSL_isservice();MessageBoxW, called byOPENSSL_showfatal().
In OpenSSL 3.0 to 3.5, both are reached only from OPENSSL_die(), the internal-error abort
path (crypto/cryptlib.c). The CAPI engine's certificate dialog also calls OPENSSL_isservice(),
but CPython does not load that engine. The import is still resolved when _hashlib.pyd loads,
and with it user32's own dependencies.
gh-155435 fixed the same thing for _ctypes (ole32), noting that a plain python.exe no longer
loads user32. gh-89883 removed shlwapi for the same kind of start-up cost. _hashlib and _ssl
still pull user32 in, through OpenSSL.
python -c "import hashlib", then tasklist /m for that process:
python.org 3.14.8 (OpenSSL 3.5.9) pass: 14 DLLs import hashlib: 24 DLLs, incl. user32 gdi32 gdi32full win32u imm32
python.org 3.15.0rc3 (OpenSSL 3.5.9) pass: 14 DLLs import hashlib: 25 DLLs, the same five
Proposed change
Link libcrypto with user32 delay-loaded, so the three functions are bound on first call, i.e. only
on the abort path. One line in PCbuild/openssl.vcxproj, which PCbuild/prepare_ssl.bat uses to
build the OpenSSL binaries:
-$(Perl) "$(opensslDir)\configure" $(OpenSSLPlatform) no-asm no-uplink
+$(Perl) "$(opensslDir)\configure" $(OpenSSLPlatform) no-asm no-uplink "LDFLAGS=/nologo /debug /DELAYLOAD:user32.dll" LDLIBS=delayimp.libLDFLAGSreplaces the VC targets' default, which is/nologo /debugin both 3.0 and 3.5.LDLIBSadds the delay-load helper. Configure still links the target's ownex_libs
(ws2_32, gdi32, advapi32, crypt32, user32).libsslimports nothing from user32. It gets a harmless LNK4199 ("/DELAYLOAD:user32.dll
ignored").
It takes effect with the next rebuild of the cpython-bin-deps OpenSSL binaries; nothing else in
the tree changes.
Measurements
Build. OpenSSL 3.5.9 (main's pin), x64, built with main's openssl.vcxproj as
prepare_ssl.bat runs it, once as is and once with the change.
Setup. Three copies of the python.org 3.15.0rc3 embeddable distribution (OpenSSL 3.5.9),
differing only in libcrypto-3.dll and libssl-3.dll:
python.org: as shipped;rebuilt: main's project as is; the control;delay-load: with the change.
Method. One machine (i9-14900K, Windows 11). 20 interleaved passes, a fresh process per
measurement. Medians, and the paired difference delay-load − rebuilt with a bootstrap 95% CI.
| python.org | rebuilt | delay-load | delay-load − rebuilt | |
|---|---|---|---|---|
import _hashlib, in-process |
4.94 ms | 4.94 ms | 2.92 ms | −1.99 ms [−2.14, −1.95], 20/20 passes |
python -c "import hashlib", start to exit |
27.0 ms | 26.8 ms | 24.7 ms | −2.3 ms [−3.0, −1.5] |
LoadLibraryExW(_hashlib.pyd) alone |
3.20 ms | 3.11 ms | 1.14 ms | −2.00 ms [−2.06, −1.90] |
| DLLs newly mapped by that load | 9 | 9 | 3 | −6 |
import ssl after _hashlib |
8.53 ms | 8.59 ms | 8.49 ms | −0.1 [−0.3, +0.1] |
python -c pass (control) |
20.9 ms | 20.9 ms | 20.7 ms | 0.0 [−0.5, +0.4] |
Reading the table.
- The control matches the python.org binaries within 0.2 ms on every row.
import ssldoes not change: the user32 cost goes to whichever of_hashliband_sslloads
libcrypto first.- The same change on a 3.14.4 PGO build with OpenSSL 3.0.19 measured −2.05 ms [−2.16, −1.95] for
import _hashlib.
Checks on 3.15.0rc3 with the delay-load libcrypto:
test_hashlib test_hmac test_ssl test_httplib test_urllib2_localnet test_asyncio.test_ssl test_asyncio.test_sslprotopass: 679 tests, 44 skipped.- The exported
OPENSSL_isservice(), called from a small console program, still works: user32 is
absent before the call, is loaded by it, and the result is the same as with the stock DLL.
In a real workflow. pip install requests==2.32.5 from sdists (build isolation, no network) starts 21 interpreters, 20 of which import _hashlib. Timing each first import _hashlib in place, the change saves 2.0 ms per process, about 40 ms per install.
Notes
- OpenSSL could instead bind these functions with
GetProcAddresson the abort path. That would
help every Windows consumer, but it is a source change in a dependency. The linker flag needs
none and is easy to drop again. - Other distributions that build their own OpenSSL can use the same flag.
Activity
picnixz commentedon Oct 8, 2026
Honestly I don't think it's worth it. cc @chris-eibl @zooba @zware We save a few ms with a change that is unlikely to be backported. I don't understand why we build in debug mode though? (or is /debug for something else?) Saving 2ms is nothing when the bottleneck is the network. But I'm not against the change if it can be explained by someone who knows Windows more
zooba commentedon Oct 8, 2026
I imagine we'd be better off avoiding libcrypto entirely for hashlib. But in any case, the build options really belong to OpenSSL - the change in the PR is to a file that we don't even use. So this should be submitted to the upstream project first before we start adding our own patches.
The /debug linker option should just control creating a .pdb file, not optimisation settings.
azchohfi commentedon Oct 13, 2026
Thanks, that makes sense. I see the release binaries come from
windows-release/openssl-build.ymlin release-tools (perl Configure ... no-uplink+nmake), so theopenssl.vcxprojchange wouldn't reach them. I'm closing #159009 and have filed this upstream as openssl/openssl#33201: the three user32 calls incrypto/cryptlib.c(inOPENSSL_isservice()andOPENSSL_showfatal()) can be resolved on first use, the same wayOPENSSL_isservice()already looks up_OPENSSL_isservicewithGetProcAddress. That drops the user32 import for every Windows consumer, with no build-flag changes here. A PR to OpenSSL will follow.