1
1
Fork
You've already forked bittricks
1

aarch64: Autodetect in test_freestanding #3

Closed
jepler wants to merge 6 commits from jepler/bittricks:aarch64-enhancements into main
pull from: jepler/bittricks:aarch64-enhancements
merge into: jjl:main
jjl:main
jjl:feat/inline-asm
Collaborator
Copy link

On aarch64, set the target architecture to a base architecture (arm8.5-a, in this case) and use attribute((target)) to enable instruction set extensions as needed on a per function basis.

There's a speculative fix for #2.

test_static fails sve2 aes prefix popcounting but at least it's successfully arch-detecting (first output is under qemu-aarch64 with advanced CPU support, second is directly on pi5, cortex-A76):

$ qemu-aarch64 ./build/test/test_static
AMD64 tests: SKIPAARCH64 tests:
Portable/aarch64 sve2 aes prefix popcounting): ERROR, no match for 8 bit on iter 0, index 0
 mask: ec
 portable: e2
 aes: 0
Portable/aarch64 neon prefix popcounting: PASS
portable/bitperm pext: PASS
portable/bitperm pdep: PASS
portable/aes pext: PASS
portable/aes pdep: PASS
portable/neon pext: PASS
portable/neon pdep: PASS
portable/bitperm pexts: PASS
portable/bitperm pdeps: PASS
portable/aes pexts: PASS
portable/aes pdeps: PASS
portable/neon pexts: PASS
portable/neon pdeps: PASS
support: 3e
$ $ ./build/test/test_static | grep -v SKIP
Portable/aarch64 neon prefix popcounting: PASS
portable/neon pext: PASS
support: 2

This is in contrast to an earlier version just crashing out because instruction set extensions were enabled across all source files & functions, which led to them being used in general test code. (something I reported on mastodon, not codeberg) (though I'm not reproducing that at the tip of your main so this needs a grain of salt)

On aarch64, set the target architecture to a base architecture (arm8.5-a, in this case) and use `attribute((target))` to enable instruction set extensions as needed on a per function basis. There's a speculative fix for #2. test_static fails sve2 aes prefix popcounting but at least it's successfully arch-detecting (first output is under qemu-aarch64 with advanced CPU support, second is directly on pi5, cortex-A76): ``` $ qemu-aarch64 ./build/test/test_static AMD64 tests: SKIPAARCH64 tests: Portable/aarch64 sve2 aes prefix popcounting): ERROR, no match for 8 bit on iter 0, index 0 mask: ec portable: e2 aes: 0 Portable/aarch64 neon prefix popcounting: PASS portable/bitperm pext: PASS portable/bitperm pdep: PASS portable/aes pext: PASS portable/aes pdep: PASS portable/neon pext: PASS portable/neon pdep: PASS portable/bitperm pexts: PASS portable/bitperm pdeps: PASS portable/aes pexts: PASS portable/aes pdeps: PASS portable/neon pexts: PASS portable/neon pdeps: PASS support: 3e $ $ ./build/test/test_static | grep -v SKIP Portable/aarch64 neon prefix popcounting: PASS portable/neon pext: PASS support: 2 ``` This is in contrast to an earlier version just crashing out because instruction set extensions were enabled across all source files & functions, which led to them being used in general test code. (something I reported on mastodon, not codeberg) (though I'm not reproducing that at the tip of your `main` so this needs a grain of salt)
Author
Collaborator
Copy link

Ah, it was specifically bench_shared that would crash in this way on Pi5 at 6102fea274, fixed by this PR.

Ah, it was specifically `bench_shared` that would crash in this way on Pi5 at 6102fea27422caeb3c9b79659cc6ff52d18ccdec, fixed by this PR.
Owner
Copy link

This largely mirrors what I've been doing for x86-64 in my in progress refactor.

As we discussed earlier, I think FMV would be preferable to an explicit IFUNC if possible. I've never actually done this before, so it's difficult to be sure, but I think if you replace the target attribute with target_version and then drop the ifuncs, it should work?

according to the ACLE spec, __HAVE_FUNCTION_MULTIVERSIONING should be set to 1 if it's present? i'm thinking maybe a macro set according to that that returns either target or target_version attribute of the provided string? and then if it's not set (but ifuncs are supported), we can provide the ifuncs. or else we fall back to the branchy impl? what do you think?

This largely mirrors what I've been doing for x86-64 in my in progress refactor. As we discussed earlier, I think FMV would be preferable to an explicit IFUNC if possible. I've never actually done this before, so it's difficult to be sure, but I *think* if you replace the `target` attribute with `target_version` and then drop the ifuncs, it should work? according to [the ACLE spec](https://arm-software.github.io/acle/main/acle.html#function-multi-versioning), __HAVE_FUNCTION_MULTIVERSIONING should be set to 1 if it's present? i'm thinking maybe a macro set according to that that returns either `target` or `target_version` attribute of the provided string? and then if it's not set (but ifuncs are supported), we can provide the ifuncs. or else we fall back to the branchy impl? what do you think?
Author
Collaborator
Copy link

I still don't know what's going on with the failure in aes prefix popcounting. This is enough of a change to provoke the error here, vs current main branch:

diff --git a/test/test_static.c b/test/test_static.c
index e1379fc..cefd6de 100644
--- a/test/test_static.c
+++ b/test/test_static.c
@@ -32,6 +32,8 @@
 uint64_t aarch64_support = 0;
 #endif
 
+#pragma GCC optimize "-O0"
+

so I think there may be something real, not caused by my changes, but hidden or changing depending on the optimization.

I am on an old gcc so maybe it's a compiler bug? I'm going to bump the OS on this pi5.

I still don't know what's going on with the failure in aes prefix popcounting. This is enough of a change to provoke the error here, vs current main branch: ``` diff --git a/test/test_static.c b/test/test_static.c index e1379fc..cefd6de 100644 --- a/test/test_static.c +++ b/test/test_static.c @@ -32,6 +32,8 @@ uint64_t aarch64_support = 0; #endif +#pragma GCC optimize "-O0" + ``` so I think there may be something real, not caused by my changes, but hidden or changing depending on the optimization. I am on an old gcc so maybe it's a compiler bug? I'm going to bump the OS on this pi5.
Owner
Copy link

there's nothing obviously wrong, but i'd start at line 964. two things:

  • i'm not 100% what the order of operations even is on that line. i should have parenthesised it better. try moving the rightmost paren to left of the shift.
  • the truncating cast to uint8_t may (i don't think does, but i am a bit hazy on this) may be causing it to overflow 8 bits? perhaps we can do that as the last thing?

this is essentially as direct a port as i could manage from zp7's pclmul technique - i looked at the operations and tried to find replacements in the arm intrinsics guide. in theory it works exactly the same way as the neon ones unless i've cocked up. but then it does seem to work depending on optimisations? not sure tbh. i'm kinda working in the dark on the sve side - i haven't been using it very long and my only way to test is extremely slowly under qemu

there's nothing *obviously* wrong, but i'd start at line 964. two things: - i'm not 100% what the order of operations even is on that line. i should have parenthesised it better. try moving the rightmost paren to left of the shift. - the truncating cast to uint8_t may (i don't think does, but i am a bit hazy on this) may be causing it to overflow 8 bits? perhaps we can do that as the last thing? this is essentially as direct a port as i could manage from zp7's pclmul technique - i looked at the operations and tried to find replacements in the arm intrinsics guide. in theory it works exactly the same way as the neon ones unless i've cocked up. but then it does seem to work depending on optimisations? not sure tbh. i'm kinda working in the dark on the sve side - i haven't been using it very long and my only way to test is extremely slowly under qemu
Author
Collaborator
Copy link

OK so I was able to test this on my telephone. It is a Pixel Pro 8 with Cortex-A510 and Cortex-A715 cores. I am testing with this PR, plus a getenv() override for the "best" algorithm...

I am running in termux which as I understand it is running binaries directly, not in emulation/virtualization.

gcc is clang version 21.1.4 / Target: aarch64-unknown-linux-android24. there's no meson package that I could find so I just build with gcc -m64 -I. -Isrc bench/bench_static.c -march=armv9-a -O3 after touch config.h lolsob

I posted some performance results on mastodon but actually performance is all over the place. throttling? idk. I have tried to pick "representative" runs, but there are also a lot of outliers as you can see from the difference in 'estimated overhead'

Core 4 (taskset mask 0x10) seems to be a performance core

~/bittricks $ for impl in 1 2 3 4; do BT_BEST_IMPL=$impl taskset 0x10 ./a.out; sleep 5;done
estimated overhead 2.864 msec ( 2.864 nsec per iteration ) last: 32073ac19a9a503e
pext: 3.841 msec ( 3.841 nsec per iteration ) last: e18a23ab2d17
pdep: 3.652 msec ( 3.652 nsec per iteration ) last: ee0ee7a56be33622
best impl: 1
estimated overhead 3.298 msec ( 3.298 nsec per iteration ) last: 405f69548cbe281c
pext: 10.419 msec ( 10.419 nsec per iteration ) last: 24b3791ea3a70
pdep: 12.992 msec ( 12.992 nsec per iteration ) last: 491a3fb234550a17
best impl: 2
estimated overhead 3.396 msec ( 3.396 nsec per iteration ) last: ab65acb3b9c5dfd7
pext: 14.105 msec ( 14.105 nsec per iteration ) last: 1c0c4c7509cc4
pdep: 17.798 msec ( 17.798 nsec per iteration ) last: aba9ee16cfad4fa0
best impl: 3
estimated overhead 2.909 msec ( 2.909 nsec per iteration ) last: 92beb379c2cdc011
pext: 18.543 msec ( 18.543 nsec per iteration ) last: 2e7a115294006
pdep: 19.769 msec ( 19.769 nsec per iteration ) last: 95dea5735ef872ed
best impl: 4

Core 0 (taskset mask 0x1) seems to be an efficiency core

estimated overhead 5.736 msec ( 5.736 nsec per iteration ) last: bfd2b5b8a4ef055f
pext: 69.811 msec ( 69.811 nsec per iteration ) last: 1b68a9c3ff479
pdep: 69.937 msec ( 69.937 nsec per iteration ) last: ff74ee5312786653
best impl: 1
estimated overhead 5.686 msec ( 5.686 nsec per iteration ) last: 4e868a9283fb2a34
pext: 69.590 msec ( 69.590 nsec per iteration ) last: 1d07a6286cf28
pdep: 87.463 msec ( 87.463 nsec per iteration ) last: 63958c5158939ce6
best impl: 2
estimated overhead 5.660 msec ( 5.660 nsec per iteration ) last: 8102d2003f90ee0c
pext: 80.168 msec ( 80.168 nsec per iteration ) last: c09c9867417c
pdep: 95.707 msec ( 95.707 nsec per iteration ) last: 37ac58b548e5220e
best impl: 3
estimated overhead 5.661 msec ( 5.661 nsec per iteration ) last: 5ddaede9d0d44f31
pext: 63.992 msec ( 63.992 nsec per iteration ) last: 931ca34be921
pdep: 76.301 msec ( 76.301 nsec per iteration ) last: 9761cf14c1f2e623
best impl: 4
OK so I was able to test this on my telephone. It is a Pixel Pro 8 with Cortex-A510 and Cortex-A715 cores. I am testing with this PR, plus a getenv() override for the "best" algorithm... I am running in termux which as I understand it is running binaries directly, not in emulation/virtualization. gcc is `clang version 21.1.4 / Target: aarch64-unknown-linux-android24`. there's no meson package that I could find so I just build with `gcc -m64 -I. -Isrc bench/bench_static.c -march=armv9-a -O3` after `touch config.h` *lolsob* I posted some performance results on mastodon but actually performance is all over the place. throttling? idk. I have tried to pick "representative" runs, but there are also a lot of outliers as you can see from the difference in 'estimated overhead' Core 4 (taskset mask 0x10) seems to be a performance core ``` ~/bittricks $ for impl in 1 2 3 4; do BT_BEST_IMPL=$impl taskset 0x10 ./a.out; sleep 5;done estimated overhead 2.864 msec ( 2.864 nsec per iteration ) last: 32073ac19a9a503e pext: 3.841 msec ( 3.841 nsec per iteration ) last: e18a23ab2d17 pdep: 3.652 msec ( 3.652 nsec per iteration ) last: ee0ee7a56be33622 best impl: 1 estimated overhead 3.298 msec ( 3.298 nsec per iteration ) last: 405f69548cbe281c pext: 10.419 msec ( 10.419 nsec per iteration ) last: 24b3791ea3a70 pdep: 12.992 msec ( 12.992 nsec per iteration ) last: 491a3fb234550a17 best impl: 2 estimated overhead 3.396 msec ( 3.396 nsec per iteration ) last: ab65acb3b9c5dfd7 pext: 14.105 msec ( 14.105 nsec per iteration ) last: 1c0c4c7509cc4 pdep: 17.798 msec ( 17.798 nsec per iteration ) last: aba9ee16cfad4fa0 best impl: 3 estimated overhead 2.909 msec ( 2.909 nsec per iteration ) last: 92beb379c2cdc011 pext: 18.543 msec ( 18.543 nsec per iteration ) last: 2e7a115294006 pdep: 19.769 msec ( 19.769 nsec per iteration ) last: 95dea5735ef872ed best impl: 4 ``` Core 0 (taskset mask 0x1) seems to be an efficiency core ``` estimated overhead 5.736 msec ( 5.736 nsec per iteration ) last: bfd2b5b8a4ef055f pext: 69.811 msec ( 69.811 nsec per iteration ) last: 1b68a9c3ff479 pdep: 69.937 msec ( 69.937 nsec per iteration ) last: ff74ee5312786653 best impl: 1 estimated overhead 5.686 msec ( 5.686 nsec per iteration ) last: 4e868a9283fb2a34 pext: 69.590 msec ( 69.590 nsec per iteration ) last: 1d07a6286cf28 pdep: 87.463 msec ( 87.463 nsec per iteration ) last: 63958c5158939ce6 best impl: 2 estimated overhead 5.660 msec ( 5.660 nsec per iteration ) last: 8102d2003f90ee0c pext: 80.168 msec ( 80.168 nsec per iteration ) last: c09c9867417c pdep: 95.707 msec ( 95.707 nsec per iteration ) last: 37ac58b548e5220e best impl: 3 estimated overhead 5.661 msec ( 5.661 nsec per iteration ) last: 5ddaede9d0d44f31 pext: 63.992 msec ( 63.992 nsec per iteration ) last: 931ca34be921 pdep: 76.301 msec ( 76.301 nsec per iteration ) last: 9761cf14c1f2e623 best impl: 4 ```
Author
Collaborator
Copy link

I know this branch is a bit of a mess. Please take any/all ideas here, and hit me up if you want me to test again. Or, I can try to tidy it up and rebase until it's pretty. Let me know!

I know this branch is a bit of a mess. Please take any/all ideas here, and hit me up if you want me to test again. Or, I can try to tidy it up and rebase until it's pretty. Let me know!
Author
Collaborator
Copy link

this also closes #2 and #4.

this also closes #2 and #4.
Owner
Copy link

i think what i'll do is try and roll it into my x86-64 changes, since i've been trying to revamp the detection logic there and it points at a few parallel changes in the aarch64 code. the main thing is that you've got this all working at all, so at least i don't have to work more stuff out 😅

thanks for all the help! i'll post again when i'm done with my refactor.

i think what i'll do is try and roll it into my x86-64 changes, since i've been trying to revamp the detection logic there and it points at a few parallel changes in the aarch64 code. the main thing is that you've got this all working at all, so at least i don't have to work more stuff out 😅 thanks for all the help! i'll post again when i'm done with my refactor.
Author
Collaborator
Copy link

again in qemu I verified that not specify the sve vector length and getting "agnostic" versions, all tested widths pass the test

$ for width in 128 512 1024 2048; do qemu-aarch64 -cpu max,sve$width=on ./build/test/test_freestanding; done | grep 'aarch64 aes'
Prefix popcounting (aarch64 aes): PASS
Prefix popcounting (aarch64 aes): PASS
Prefix popcounting (aarch64 aes): PASS
Prefix popcounting (aarch64 aes): PASS

I don't know what it means, and qemu benchmarks are nearly meaningless, but I noticed performance depends on the emulated sve bit width .. up to a point

$ for width in 128 256 512 1024 2048; do qemu-aarch64 -cpu max,sve$width=on ./build/bench/bench_static;sleep 1; done| grep pext
pext: 470.223 msec ( 470.223 nsec per iteration ) last: 3909c8d701da3
pext: 674.987 msec ( 674.987 nsec per iteration ) last: 49b8f7fe0fbef
pext: 1.044 secs ( 1.044 usec per iteration ) last: 64e44670c2143
pext: 1.025 secs ( 1.025 usec per iteration ) last: 22a1f3fb97bc9
pext: 1.023 secs ( 1.023 usec per iteration ) last: 2b54b47441124

of course on a real CPU you don't tune the sve width at runtime, as far as I understand.

again in qemu I verified that *not* specify the sve vector length and getting "agnostic" versions, all tested widths pass the test ``` $ for width in 128 512 1024 2048; do qemu-aarch64 -cpu max,sve$width=on ./build/test/test_freestanding; done | grep 'aarch64 aes' Prefix popcounting (aarch64 aes): PASS Prefix popcounting (aarch64 aes): PASS Prefix popcounting (aarch64 aes): PASS Prefix popcounting (aarch64 aes): PASS ``` I don't know what it means, and qemu benchmarks are nearly meaningless, but I noticed performance depends on the emulated sve bit width .. up to a point ``` $ for width in 128 256 512 1024 2048; do qemu-aarch64 -cpu max,sve$width=on ./build/bench/bench_static;sleep 1; done| grep pext pext: 470.223 msec ( 470.223 nsec per iteration ) last: 3909c8d701da3 pext: 674.987 msec ( 674.987 nsec per iteration ) last: 49b8f7fe0fbef pext: 1.044 secs ( 1.044 usec per iteration ) last: 64e44670c2143 pext: 1.025 secs ( 1.025 usec per iteration ) last: 22a1f3fb97bc9 pext: 1.023 secs ( 1.023 usec per iteration ) last: 2b54b47441124 ``` of course on a real CPU you don't tune the sve width at runtime, as far as I understand.
Owner
Copy link

I think I've covered off everything you did. One thing that I changed is that I do not set any compiler arguments in meson.build now by default (except for tests and benches).

Oh and there are now IFUNCS for aarch64, so you should have plenty of fun things to test

I think I've covered off everything you did. One thing that I changed is that I do not set any compiler arguments in meson.build now by default (except for tests and benches). Oh and there are now IFUNCS for aarch64, so you should have plenty of fun things to test
Owner
Copy link

I don't know what it means, and qemu benchmarks are nearly meaningless, but I noticed performance depends on the emulated sve bit width .. up to a point

i think qemu's max vector width is 512.

of course on a real CPU you don't tune the sve width at runtime, as far as I understand.

indeed, the vendor chooses a register size before selling you the product. most are using 128 bit, but there are a handful of 256 bit and i think only a single 512 bit (the fugaku supercomputer, so not exactly common ;) )

> I don't know what it means, and qemu benchmarks are nearly meaningless, but I noticed performance depends on the emulated sve bit width .. up to a point i think qemu's max vector width is 512. > of course on a real CPU you don't tune the sve width at runtime, as far as I understand. indeed, the vendor chooses a register size before selling you the product. most are using 128 bit, but there are a handful of 256 bit and i think only a single 512 bit (the fugaku supercomputer, so not exactly common ;) )
jepler closed this pull request 2026年03月17日 16:53:15 +01:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.
No reviewers
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jjl/bittricks!3
Reference in a new issue
jjl/bittricks
No description provided.
Delete branch "jepler/bittricks:aarch64-enhancements"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?