-
Notifications
You must be signed in to change notification settings - Fork 951
compiler: zero struct padding during map operations #3437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
More testing and cleanup required. But otherwise I'm happy with this. |
1ca9e1e
to
ace35c5
Compare
Fixes the test case from #3358. |
Still need to come up with a proper test case to add, but I think this is ready for review otherwise. |
@anuraaga Can you test this fix in your application? |
Seems to be good, thanks @dgryski! |
@aykevl any comments here before merge? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR should really get some tests. Perhaps in a new compiler/testdata/map.go, with tests like:
type paddedStruct struct {
byte a
int b
}
func storeToPaddedStruct(m map[paddedStruct]int, key paddedStruct {
m[key] = 3
}
(Didn't test this code and whether it reproduces the issue, but I think it does).
Also a suggestion: you could use LLVM types throughout instead of Go types. That is, use llvm.Type
instead of types.Type
for the typ
parameter. That might be a little bit more reliable as it certainly catches all types that need explicit zeroing for the padding (in case a Go type is ever implemented as a struct with padding).
The zeroing algorithm itself looks reasonable to me.
I'll update this to to use llvm types instead of Go types. Also I tried to create a unit test for this, and while I could create a case where I was accessing a map with a padded struct, I couldn't cause it to have non-zero padding bytes. |
ace35c5
to
b50a471
Compare
I think I addressed all the issues. Need to run this through the test corpus. Still need to write an actual test. |
b50a471
to
0b873f5
Compare
Thinking some more, I think you also need to zero the end of structs. For example, with this one: type someStruct struct {
a float32
b char
} This struct is 8 bytes, but only the first 5 are written to.
Unit tests would be great, but are pretty difficult in the case of undefined behavior. |
Ah yes. When I was looking at the code I assumed that |
7b537f1
to
1e40948
Compare
Now with a unit test. PTAL. |
Passes the test corpus, which is always nice. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tried running a few tests under LLVM 14? LLVM 14 still requires pointer types to match, so I think you will hit a few validation errors. (We don't really have such a test in CI, although I think it would be caught if a padded struct gets added to testdata/map.go - maybe that's a good idea anyway).
(The "Suggestion" comments are really just that, style suggestions you can apply if you like them).
compiler/map.go
Outdated
|
||
for i := 0; i < llvmArrayType.ArrayLength(); i++ { | ||
idx := llvm.ConstInt(b.uintptrType, uint64(i), false) | ||
elemPtr := b.CreateGEP(llvmArrayType, ptr, []llvm.Value{idx}, "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use CreateInBoundsGEP
here (and other places in this PR), because you know the GEP is going to be in bounds. ("in bounds", as I understand it, basically means you are 100% sure the pointer is still pointing inside the allocated object - in this case, the alloca).
Also, I believe you need to add a zero index as the first index parameter to the GEP, like here:
Lines 2020 to 2021 in 0d56dee
zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) | |
ptr := b.CreateInBoundsGEP(arrayType, alloca, []llvm.Value{zero, index}, "index.gep") |
More information: https://llvm.org/docs/GetElementPtr.html
compiler/map.go
Outdated
numFields := llvmStructType.StructElementTypesCount() | ||
llvmElementTypes := llvmStructType.StructElementTypes() | ||
|
||
for i := 0; i < llvmStructType.StructElementTypesCount(); i++ { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion, I think this looks better:
for i := 0; i < llvmStructType.StructElementTypesCount(); i++ { | |
for i := 0; i < numFields; i++ { |
Or even:
for i := 0; i < llvmStructType.StructElementTypesCount(); i++ { | |
for i, llvmElemType := range llvmElementTypes { |
compiler/map.go
Outdated
for i := 0; i < llvmStructType.StructElementTypesCount(); i++ { | ||
offset := b.targetData.ElementOffset(llvmStructType, i) | ||
llvmOffset := llvm.ConstInt(b.uintptrType, offset, false) | ||
elemPtr := b.CreateGEP(b.ctx.Int8Type(), ptr, []llvm.Value{llvmOffset}, "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line will likely lead to a verifier failure in LLVM 14. To fix it, you need to cast ptr
to an i8*
. You can use b.CreateBitCast(ptr, b.i8ptrType, "")
to cast the pointer type. And cast back to the pointer element type after the GEP.
compiler/map.go
Outdated
if storeSize := b.targetData.TypeStoreSize(llvmElemType); offset+storeSize != nextOffset { | ||
n := llvm.ConstInt(b.uintptrType, nextOffset-(offset+storeSize), false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion. Doing the calculation only once may be easier to read.
if storeSize := b.targetData.TypeStoreSize(llvmElemType); offset+storeSize != nextOffset { | |
n := llvm.ConstInt(b.uintptrType, nextOffset-(offset+storeSize), false) | |
gap := nextOffset-(offset+b.targetData.TypeStoreSize(llvmElemType)) | |
if gap > 0 { | |
n := llvm.ConstInt(b.uintptrType, gap, false) |
compiler/map.go
Outdated
if storeSize := b.targetData.TypeStoreSize(llvmElemType); offset+storeSize != nextOffset { | ||
n := llvm.ConstInt(b.uintptrType, nextOffset-(offset+storeSize), false) | ||
llvmSize := llvm.ConstInt(b.uintptrType, storeSize, false) | ||
paddingStart := b.CreateGEP(b.ctx.Int8Type(), elemPtr, []llvm.Value{llvmSize}, "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another GEP that will likely fail in LLVM 14.
compiler/map.go
Outdated
// zero any padding bytes between end of last field and end of struct | ||
allocSize := b.targetData.TypeAllocSize(llvmStructType) | ||
lastElemOffset := b.targetData.ElementOffset(llvmStructType, numFields-1) | ||
lastElemStore := b.targetData.TypeStoreSize(llvmElementTypes[numFields-1]) | ||
|
||
if allocSize != lastElemOffset+lastElemStore { | ||
n := llvm.ConstInt(b.uintptrType, allocSize-(lastElemOffset+lastElemStore), false) | ||
llvmSize := llvm.ConstInt(b.uintptrType, lastElemOffset+lastElemStore, false) | ||
paddingStart := b.CreateGEP(b.ctx.Int8Type(), ptr, []llvm.Value{llvmSize}, "") | ||
b.createRuntimeCall("memzero", []llvm.Value{paddingStart, n}, "") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could consider merging these in the for loop above, with logic like this (in pseudocode):
if last field:
nextOffset = one-past-the-end of the struct
else:
nextOffset = start offset of next field
if offset + TypeAllocSize(field) != nextOffset:
zero padding
Not sure it's more readable though.
compiler/testdata/zeromap.go
Outdated
func main() { | ||
m := make(map[hasPadding]int) | ||
var s hasPadding | ||
|
||
for i := 0; i < 10; i++ { | ||
s.b1 = i&1 == 0 | ||
s.i = i | ||
s.b2 = i&1 == 1 | ||
m[s]++ | ||
} | ||
println(len(m)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: use the following code to test:
func testZeroGet(m map[hasPadding]int, s hasPadding) int {
return m[s]
}
func testZeroSet(m map[hasPadding]int, s hasPadding) {
m[s] = 5
}
This makes zeromap.ll smaller and more readable while still testing the important parts (the calls to runtime.memzero
). For example, testZeroGet
compiles to:
define hidden i32 @main.testZeroGet(ptr dereferenceable_or_null(40) %m, i1 %s.b1, i32 %s.i, i1 %s.b2, ptr %context) unnamed_addr #1 {
entry:
%hashmap.key = alloca %main.hasPadding, align 8
%hashmap.value = alloca i32, align 4
%s = alloca %main.hasPadding, align 8
%0 = insertvalue %main.hasPadding zeroinitializer, i1 %s.b1, 0
%1 = insertvalue %main.hasPadding %0, i32 %s.i, 1
%2 = insertvalue %main.hasPadding %1, i1 %s.b2, 2
%stackalloc = alloca i8, align 1
store %main.hasPadding zeroinitializer, ptr %s, align 8
call void @runtime.trackPointer(ptr nonnull %s, ptr nonnull %stackalloc, ptr undef) #3
store %main.hasPadding %2, ptr %s, align 8
call void @llvm.lifetime.start.p0(i64 4, ptr nonnull %hashmap.value)
call void @llvm.lifetime.start.p0(i64 12, ptr nonnull %hashmap.key)
store %main.hasPadding %2, ptr %hashmap.key, align 8
%3 = getelementptr inbounds i8, ptr %hashmap.key, i32 1
call void @runtime.memzero(ptr nonnull %3, i32 3, ptr undef) #3
%4 = getelementptr inbounds i8, ptr %hashmap.key, i32 9
call void @runtime.memzero(ptr nonnull %4, i32 3, ptr undef) #3
%5 = call i1 @runtime.hashmapBinaryGet(ptr %m, ptr nonnull %hashmap.key, ptr nonnull %hashmap.value, i32 4, ptr undef) #3
call void @llvm.lifetime.end.p0(i64 12, ptr nonnull %hashmap.key)
%6 = load i32, ptr %hashmap.value, align 4
call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %hashmap.value)
ret i32 %6
}
Also, it might be worth testing an array as well (it could just be a [2]hasPadding
).
Trying to get the |
4ef3a42
to
56c05c3
Compare
5df309c
to
da19745
Compare
OK. Switched to |
Did a quick check and this PR as-is is indeed not working in LLVM 14:
Will take a more in-depth look later. |
I have LLVM 14 from checking out the reflect fixes. I'll look at this now. |
Didn't fully test this but it seems to fix all the verification failures for me in LLVM 14: --- a/compiler/map.go
+++ b/compiler/map.go
@@ -325,7 +325,14 @@ func (b *builder) zeroUndefBytes(llvmType llvm.Type, ptr llvm.Value) error {
if fieldEndOffset != nextOffset {
n := llvm.ConstInt(b.uintptrType, nextOffset-fieldEndOffset, false)
llvmStoreSize := llvm.ConstInt(b.uintptrType, storeSize, false)
- paddingStart := b.CreateInBoundsGEP(b.i8ptrType, elemPtr, []llvm.Value{llvmStoreSize}, "")
+ gepPtr := elemPtr
+ if gepPtr.Type() != b.i8ptrType {
+ gepPtr = b.CreateBitCast(gepPtr, b.i8ptrType, "") // LLVM 14
+ }
+ paddingStart := b.CreateInBoundsGEP(b.ctx.Int8Type(), gepPtr, []llvm.Value{llvmStoreSize}, "")
+ if paddingStart.Type() != b.i8ptrType {
+ paddingStart = b.CreateBitCast(paddingStart, b.i8ptrType, "") // LLVM 14
+ }
b.createRuntimeCall("memzero", []llvm.Value{paddingStart, n}, "")
}
} |
Explanation:
Check for LLVM 14. In LLVM 14, every pointer is different (with different element types), while in LLVM 15 all pointers in the same address space are the same.
Convert to |
PTAL. |
Fixes #3358