Skip to content

Carry pointee alignment on struct copies through pointers and refs - #5297

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

TurkeyMan wants to merge 1 commit into
ldc-developers:masterfrom
TurkeyMan:lvalue-alignment

Conversation

@TurkeyMan

@TurkeyMan TurkeyMan commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Struct assignment and struct zero/static initialisation emit llvm.memcpy/llvm.memset with alignment 1. Wherever LLVM can't see where a pointer came from (anything reached through a pointer, slice or ref), every struct copy is treated as potentially unaligned. On strict-alignment targets (e.g. -mtriple=armv5te-none-eabi -mattr=+strict-align) each copy then lowers to a run of ldrb/strb.

This PR makes DLValue record the alignment its pointer is guaranteed to have, defaulting to 1 (unknown), and sets it only where it follows from the pointer's type:

  • *p, p[i], slice[i]
  • ref parameters, ref returns, ref locals
  • this in struct methods

Each of these takes the pointee type's alignment (DtoAlignment(T), so align(1) struct stays at 1 and align(16) struct gets 16). That's the assumption LDC already makes for every scalar load and store through such pointers. Struct copies use the recorded alignment of each side separately (DtoMemCpy now takes a destination and a source alignment), and zero/static initialisation uses the destination's.

dst = src;   // ref S8 dst, ref S8 src
before: call void @llvm.memcpy.p0.p0.i64(ptr align 1 %dst, ptr align 1 %src, i64 16, i1 false)
after:  call void @llvm.memcpy.p0.p0.i64(ptr align 8 %dst, ptr align 8 %src, i64 16, i1 false)

Not covered here: field addresses (s.f, obj.f) and static array elements stay at 1. Their alignment depends on the containing aggregate's alignment and the field offset (MinAlign(base, offset)), and doing that properly means scalar loads and stores should honour it too. Today they always assume the ABI alignment, so a uint field in an align(1) struct is loaded with align 4. I'll do that as a follow-up. That follow-up depends on dlang/dmd#23966: UnionExp emplaces RealExp/ComplexExp into storage aligned to 8 while real needs 16 on x86_64 posix, which an earlier version of this change (#5279) tripped over in the self-hosted build.

Tested with the new tests/codegen/struct_copy_alignment.d and the lit suite. I also built ldc2 with the patched compiler and compiled Phobos' std/stdio.d unittests with it using the CI flags, the step where #5279 failed.

@TurkeyMan

Copy link
Copy Markdown
Contributor Author

This patch begins a more comprehensive take on #5279, which handles a key edge cases with proper alignment tracking. LDC has a serious case of forgetting alignment throughout the flow. There's another one after this which similarly treads some other territory.

@kinke

kinke commented Oct 5, 2026

Copy link
Copy Markdown
Member

Thanks, that looks better - and storing the alignment in the DLValue makes sense to me.

That follow-up depends on dlang/dmd#23966

Cool, thx for tracking down and fixing that one.

Comment thread gen/llvmhelpers.cpp Outdated
Comment on lines +220 to +222
const auto minAlignment =
std::min(DtoAlignment(val->type), static_cast<unsigned>(alignment));
DtoMemCpy(copy, lval, DtoConstSize_t(minSize), minAlignment);
DtoMemCpy(copy, lval, DtoConstSize_t(minSize), minAlignment, minAlignment);

@kinke kinke Oct 5, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

alignment is the destination alignment, val's (new) the source one - no need for minAlignment anymore due to the split alignments now.

Comment thread gen/llvmhelpers.cpp Outdated
"made to.");
return new DLValue(type, getIrValue(vd));
return new DLValue(type, getIrValue(vd),
vd->isReference() ? DtoAlignment(type) : 1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

DtoAlignment(type) should be fine for non-refs too, propagating the alignment of the alloca.

Comment thread gen/nested.cpp Outdated
src = indexVThis(ad, thisptr);
}
if (depth > 1) {
const unsigned ptrAlign = getABITypeAlign(getOpaquePtrType());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IIRC, we use target.ptrsize as pointer alignment in other places already.

Comment thread gen/tocall.cpp
if (retValIsLVal) {
return new DLValue(resulttype, retllval);
return new DLValue(resulttype, retllval,
tf->isRef() ? DtoAlignment(resulttype) : 1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

DtoAlignment(resulttype) should be fine for non-refs too (in which case - retValIsLVal - the lvalue has been allocated by the caller on its stack, and that alloca has the type alignment).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

retllval can be in-place construction's sret, for a field which may be align(1). I have a follow-up patch coming which works through a broader set of cases though, and removes this again with that.

Comment thread gen/toir.cpp
LLValue *v = p->func()->thisArg;
result = new DLValue(e->type, v);
const bool isStruct = e->type->toBasetype()->ty == TY::Tstruct;
result = new DLValue(e->type, v, isStruct ? DtoAlignment(e->type) : 1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For classes, it's the ClassDeclaration::alignsize IIRC.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For classes this lvalue is the stack slot holding this, so it's pointer-aligned. alignsize applies to field accesses through the reference; that comes in the follow-up I mentioned above, and it needs dmd #23966... which was my next question; how do we make a change to LDC which depends on a DMD update?

Comment thread gen/toir.cpp Outdated
LLValue *initsym = getIrAggr(sd)->getInitSymbol();
assert(dstMem->getType() == initsym->getType());
DtoMemCpy(DtoType(e->type), dstMem, initsym);
DtoMemCpy(DtoType(e->type), dstMem, initsym, false, dstAlign);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The init symbol's alignment is the type's.

Comment thread gen/toir.cpp Outdated
static DLValue *emitStructLiteral(StructLiteralExp *e,
LLValue *dstMem = nullptr) {
LLValue *dstMem = nullptr,
unsigned dstAlign = 1) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please no default value here, this is an internal helper called twice, so the call sites should take care of passing an alignment.

Struct assignment and struct zero/static initialisation emitted
llvm.memcpy/llvm.memset with alignment 1, so LLVM had to treat every
struct copy as potentially unaligned whenever it could not see where
the pointer came from. On strict-alignment targets that lowers each
copy to byte loads and stores.

DLValue now records the alignment its pointer is guaranteed to have,
defaulting to 1 (unknown). Lvalues formed by dereferencing a pointer
(*p, p[i], slice[i]), ref returns, ref locals and the `this` of struct
methods take the pointee type's alignment, which LDC already assumes for
every scalar load and store through such pointers. Parameters and init
symbols take their type's alignment, as their storage is allocated for it.
Struct copies and zero-initialisations use the recorded alignment of
each side.

Field addresses and static array elements stay at 1 for now; their
alignment depends on the containing aggregate's alignment and the
field offset.
@TurkeyMan

Copy link
Copy Markdown
Contributor Author

Fixed most of those things, 2 should stay as they are until the follow-up. How do we update DMD though? As soon as the alignments all flow properly, DMD reveals that bug.

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.

2 participants