Skip to content

Carry the D type alignment on struct copy/init memcpy and memset - #5279

Open
TurkeyMan wants to merge 1 commit into
ldc-developers:masterfrom
TurkeyMan:struct-copy-alignment
Open

Carry the D type alignment on struct copy/init memcpy and memset#5279
TurkeyMan wants to merge 1 commit into
ldc-developers:masterfrom
TurkeyMan:struct-copy-alignment

Conversation

@TurkeyMan

Copy link
Copy Markdown
Contributor

DtoAssign copies structs through DtoMemCpy(DtoType(lhs->type), dst, src), which uses the helper's default alignment of 1. Struct literal initialisation (getInitSymbol() copy and the zero-init path) and struct zero-assignment do the same. Every struct copy therefore reaches LLVM as an llvm.memcpy / llvm.memset with align 1, and LLVM has to treat it as potentially unaligned.

On targets that can't do unaligned access this is costly: with -mtriple=armv5te-none-eabi -mattr=+strict-align, each small struct copy is lowered to a run of ldrb/strb (or store i64 ..., align 1 after SROA, which lowers the same way). Example, appending a plain struct { uint; long; delegate } to an array:

strb r6, [r0, r1, lsl #3]!
lsr  r2, r1, #8
strb r2, [r0, #21]
lsr  r2, r3, #8
strb r2, [r0, #17]
... (30 more)

versus five strs with the alignment known. On a ~1 MB embedded image (OpenWatt, LDC 1.43.0, -Oz) this accounted for roughly 12 KB of text, about 60% of the whole strict-align penalty.

This PR passes DtoAlignment(type) at those sites. That is the same alignment LDC already uses for the type's allocas and globals, so no new assumption is introduced: an align(1) struct still copies with alignment 1, and an align(16) struct now says 16.

Before / after for the added test:

call void @llvm.memcpy.p0.p0.i64(ptr align 1 %dst, ptr align 1 %src, i64 16, i1 false)
call void @llvm.memcpy.p0.p0.i64(ptr align 8 %dst, ptr align 8 %src, i64 16, i1 false)

Not touched here: array slice copies (arrays.cpp), tuple element copies and va_copy, which go through the untyped helper or don't have the D type at hand.

DtoAssign copied structs via DtoMemCpy with the default alignment of 1,
and struct literal initialisation (static init symbol copy and zero-init)
did the same. LLVM then had to treat every struct copy as potentially
unaligned. On targets without unaligned access (e.g. armv5te with
-mattr=+strict-align) that lowers each small struct copy to a sequence of
byte loads and stores; on a ~1 MB embedded image this was ~12 KB of text.

Pass DtoAlignment(type) at those sites. This is the alignment the type's
allocas and globals already use, so no new assumption is introduced; an
align(1) struct still copies with alignment 1.
@TurkeyMan
TurkeyMan force-pushed the struct-copy-alignment branch from 16108da to fe10226 Compare September 2, 2026 16:21
@TurkeyMan

Copy link
Copy Markdown
Contributor Author

Lol! Fable just went and did this all on its own... I had no input here! It determined that an issue was LDC, and then decided that the best resolution was to patch LDC, then it fetched and built LDC with its patch, tested it, and now here we are! I only saw the link to this PR in its progress summary.

That said, I actually think this is legit, but I can't comment at all on the merit of its fix.
I'm seeing platforms that don't support unaligned load/store generating absolutely horrific code in certain situations. The compiler seems to lose confidence in memory alignment at surprising locations, and then LLVM falls back to a series of byte load/stores, and then reassemble with shifts and or's, and that process on ARM chips with 32bit opcodes can add substantial bloat to otherwise trivial functions. My binary grew by ~2-3% due to unnecessary alignment-related bloat (a few 10s kB on a ~900k binary). It's actually worth a couple of material features!

@TurkeyMan

Copy link
Copy Markdown
Contributor Author

Also, I know we only just released like, a couple of days ago... but if you choose to merge this, a point-release would be REALLY appreciated, otherwise I'll need to maintain my own compiler binaries for the whole next development cycle... which particularly sucks when it's also pulled by Github CI workers.

@thewilsonator

Copy link
Copy Markdown
Contributor

That said, I actually think this is legit, but I can't comment at all on the merit of its fix.

if you wanted to verify this, compare this to what clang does.
If clang does do this, then we should also very probably do this.

I have no objections to this PR, but will wait until after dconf to merge it because I'm busy and brain is still jet lagged.

@TurkeyMan

Copy link
Copy Markdown
Contributor Author

Claude said that this is exactly what Clang does, that was his key evidence that this was the right thing to do when I interrogated him.

@kinke

kinke commented Sep 3, 2026

Copy link
Copy Markdown
Member

I have no objections to this PR, but will wait until after dconf to merge it

You might have noticed that CI is red, there's nothing to merge yet. Feel free to merge dcompute PRs (preferably only touching the dcompute parts), and approve of other PRs, but please leave merging these other PRs to Johan and me; I want to keep a certain degree of quality.

That said, the changes here look good at first glance, except for CI and the changelog; I don't consider this a bugfix, nor worthy of a changelog entry. We should probably revise all memcpys and enable the nowadays 2 separate alignments for dst/src at one point.

A variable can have its own alignment, overriding the type's; that would probably need to be handled too and might be the cause for the CI failures (for the compiler itself).

@thewilsonator

Copy link
Copy Markdown
Contributor

You might have noticed that CI is red, there's nothing to merge yet.

You will also note that the CI traces fail in the frontend

LLVM_SYMBOLIZER_PATH` to point to it):
    0  ldc2                     0x000000010d0a2528 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) + 40
    1  ldc2                     0x000000010d09fee7 llvm::sys::RunSignalHandlers() + 183
    2  ldc2                     0x000000010d0a3152 SignalHandler(int, __siginfo*, void*) + 290
    3  libsystem_platform.dylib 0x00007ff8063983bd _sigtramp + 29
    4  (error)                  0x000002e8d617f2c0
    5  ldc2                     0x000000010d487e0f _D3dmd8ctfeexpr10ctfeRawCmpFSQBb8location3LocCQBs10expression10ExpressionQBcbZi + 1791
     #0 0x0000000826e03899 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) (/usr/local/llvm19/lib/libLLVM.so.19.1+0x3c03899)
     #1 0x0000000826e01727 llvm::sys::RunSignalHandlers() (/usr/local/llvm19/lib/libLLVM.so.19.1+0x3c01727)
     #2 0x0000000826e03fa7 (/usr/local/llvm19/lib/libLLVM.so.19.1+0x3c03fa7)
     #3 0x000000082c21b8a0 (/lib/libthr.so.3+0x1a8a0)
     #4 0x000000082c21ae6b (/lib/libthr.so.3+0x19e6b)
     #5 0x00000008220172d3 ([vdso]+0x2d3)
     #6 0x0000000000e4241c _D3dmd9constfold4CastFSQv8location3LocCQBl5mtype4TypeQpCQCc10expression10ExpressionZSQDf8ctfeexpr8UnionExp (/home/runner/work/ldc/build/bin/ldc2+0xe4241c)

and this is very clearly a IR gen change. I'm not quite sure why those targets fail, but it is clearly unrelated to this. Perhaps from the recent DMD version update?

@kinke

kinke commented Sep 3, 2026

Copy link
Copy Markdown
Member

Clearly unrelated to this?! This is the new compiler having compiled itself and then crashing when compiling the std.stdio unittests. So it looks as if the frontend itself contains problematic stuff that fails with these changes.

@TurkeyMan

Copy link
Copy Markdown
Contributor Author

Please take ownership of this PR, if it needs amendment or another approach. It looks straightforward enough, but like you said, I wouldn't be at all surprised if it misses detail that someone who actually knows the compiler would know about!

This makes a huge difference to the final binary though, I've built my code with this patch and it's seriously better codegen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants