Alignment of 64-bit local variable on the 32-bit architecture

313 views
Skip to first unread message

Michal Derkacz

unread,
Apr 22, 2024, 7:03:42 AMApr 22
to golang-dev
In Go 1.22.2 there is a test in the runtime/internal/atomic which looks like below:

func TestAnd64(t *testing.T) {
// Basic sanity check.
x := uint64(0xffffffffffffffff)
for i := uint64(0); i < 64; i++ {
old := x
v := atomic.And64(&x, ^(1 << i))
if r := uint64(0xffffffffffffffff) << (i + 1); x != r || v != old {
t.Fatalf("clearing bit %#x: want %#x, got new %#x and old %#v", uint64(1<<i), r, x, v)
}
}
// ...
}

It pass on linux/arm but fails on my linux/thumb port because of the lack of 64-bit alignment. In both cases the x doesn't escape and stays on the stack, but in case of arm it seems always to be 64-bit aligned.

I can broke this test by adding another local uint32 variable after or before x.

Is there something that guarantees that the x is always 64-bit aligned or is it a bug, that unfortunately doesn't reveal itself in this test?

According to my findings the max. alignment guaranteed on the stack is RegSize but maybe I missed something.

Jorropo

unread,
Apr 22, 2024, 7:49:21 AMApr 22
to Michal Derkacz, golang-dev
I think this is correct, I had this issue when playing around with SIMD in amd64, gcc / LLVM backends can manually align stack to by adding an offset and anding the lower bits of SP away but in my case implementing this was non trivial and I just used the unaligned memory operations instead.

--
You received this message because you are subscribed to the Google Groups "golang-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-dev+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-dev/072a7397-4459-46bd-8fbc-b725d92ab069n%40googlegroups.com.

Michal Derkacz

unread,
Apr 22, 2024, 8:00:43 AMApr 22
to golang-dev
Hmm... What is correct?

The assumption that x is always 64-bit aligned?

atomic.And64 requires 64-bit alignment and panics otherwise.

Jorropo

unread,
Apr 22, 2024, 9:15:38 AMApr 22
to Michal Derkacz, golang-dev
I think it is correct that we don't currently support aligning to bigger than regsize.
sync/atomic document this works because it make atomic operations leak:
> On ARM, 386, and 32-bit MIPS, it is the caller's responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically via the primitive atomic functions (types Int64 and Uint64 are automatically aligned). The first word in an allocated struct, array, or slice; in a global variable; or in a local variable (because the subject of all atomic operations will escape to the heap) can be relied upon to be 64-bit aligned.

Russ Cox

unread,
Apr 22, 2024, 10:01:02 AMApr 22
to Michal Derkacz, golang-dev
On Mon, Apr 22, 2024 at 7:03 AM Michal Derkacz <mic...@lnet.pl> wrote:
In Go 1.22.2 there is a test in the runtime/internal/atomic which looks like below:

func TestAnd64(t *testing.T) {
// Basic sanity check.
x := uint64(0xffffffffffffffff)
for i := uint64(0); i < 64; i++ {
old := x
v := atomic.And64(&x, ^(1 << i))
if r := uint64(0xffffffffffffffff) << (i + 1); x != r || v != old {
t.Fatalf("clearing bit %#x: want %#x, got new %#x and old %#v", uint64(1<<i), r, x, v)
}
}
// ...
}

It pass on linux/arm but fails on my linux/thumb port because of the lack of 64-bit alignment. In both cases the x doesn't escape and stays on the stack, but in case of arm it seems always to be 64-bit aligned.

It's surprising that it doesn't escape. On 32-bit platforms we have historically relied on the arguments to atomic function escaping, because then they get heap allocated, which aligns 64-bit allocations on 64-bit boundaries.

Stack variables can't easily be aligned to 64-bit boundaries on 32-bit platforms, because the stack frame is not itself 64-bit aligned on those systems. 

If there has been a change to make the atomic operations not escape their arguments, that change should probably be disabled on 32-bit systems.

Best,
Russ

Michal Derkacz

unread,
Apr 22, 2024, 12:21:42 PMApr 22
to golang-dev
And64 inlines to the call to the Cas64 assembly function on arm and it has the go:noescape directive:

//go:noescape
func Cas64(addr *uint64, old, new uint64) bool

Russ Cox

unread,
Apr 22, 2024, 1:04:12 PMApr 22
to Michal Derkacz, golang-dev
Ah, I missed that this was only in runtime/internal/atomic. The sync/atomic package doesn't seem to have that problem.

Inside the runtime, we are willing to be a little more aggressive about optimizations, since we control all the code that can possibly be affected. If only the test is failing on your linux/thumb port, it is probably okay to keep the go:noescape tags and fix the test. For example, we could add:

var global any

func escape(ptr any) {
    if global != nil {
        global = ptr
    }
}

to the test file, and then have TestAnd64 call escape(&x). 

Best,
Russ




--
You received this message because you are subscribed to the Google Groups "golang-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-dev+...@googlegroups.com.

Michal Derkacz

unread,
Apr 22, 2024, 2:20:46 PMApr 22
to golang-dev
Let's leave my thumb port aside.

The question is does the current code in TestAnd64 contains a bug and only pass on arm and any other 32-bit architecture because of a bit of luck?
 
Message has been deleted

Michal Derkacz

unread,
Apr 23, 2024, 11:48:40 AMApr 23
to golang-dev
Benchmarks in the  runtime/internal/atomic/bench_test.go use simple

var sink any

func BenchmarkAtomicLoad64(b *testing.B) {
var x uint64
sink = &x
for i := 0; i < b.N; i++ {
_ = atomic.Load64(&x)
}
}

This sink = &x should be probably also used in the atomic_andor_test.go.

Egon Elbre

unread,
Apr 27, 2024, 5:24:39 AMApr 27
to golang-dev
Yes, it seems it was passing due to luck. https://github.com/golang/go/issues/67077
Reply all
Reply to author
Forward
0 new messages