jcavar
(Josip Cavar)
1
Here is a short snippet of code that triggers violation:
"Using type 'Int32' can cause metadata allocation or locks"
@_noLocks
func notEqual() -> Bool {
let x = Int32(1)
return x != 1
}
This behaviour is specific to Int32, UInt32, Int16, ... variants and only when using inequality check. ==, >, < work without compilation issue.
Is this genuine issue and what is specific about inequality check for these types that can cause lock?
Also, is there a good way for me (end user) to understand what compiler is doing here?
7 Likes
Jon_Shier
(Jon Shier)
2
Seems to work fine for me on godbolt (make sure you use -experimental-performance-annotations). Have you tried !(x == 1)?
2 Likes
jcavar
(Josip Cavar)
3
Seems to work fine for me on godbolt
Thanks for mentioning that. I guess it might be annotation implementation bug in that case?
Though, it interesting, this is when using Int:
output.notEqual() -> Swift.Bool:
push rbp
mov rbp, rsp
xor eax, eax
pop rbp
ret
and this is when using Int32, Int64, ...:
output.notEqual() -> Swift.Bool:
push rbp
mov rbp, rsp
nop
xor eax, eax
pop rbp
ret
There is extra nop instruction.
(make sure you use -experimental-performance-annotations )
Yes, I am using -experimental-performance-annotations
Have you tried !(x == 1)
This compiles ok.
tera
4
I can reproduce this in Xcode, looks like a bug.
Interestingly:
return x != 1 // 🛑 Using type 'Int32' can cause metadata allocation or locks
but:
return x != Int32(1) // ✅
return x != (1 as Int32) // ✅
May be related:
// without @_noLocks
func foo() {
Int32(1) != 0x123456789 // ✅
Int32(1) == 0x123456789 // 🛑 Integer literal '4886718345' overflows when stored into 'Int32'
// and then this!
Int32(1) != Int(0x123456789) // ✅
Int32(1) == Int(0x123456789) // ✅
}
3 Likes
xwu
(Xiaodi Wu)
5
Yikes. This is one of a long line of problems with comparison (and other) operators that support heterogeneous types.
These are supposed to have been stamped out by brute force for concrete values using additional concretely typed overloads (the problem has never been solved in generic code), but something must have caused a regression. As a result, != is preferring heterogeneous generic comparison to the default integer type over homogeneous (same-type) comparison.
It is more than a performance annotation issue but an outright correctness issue:
Int32.min == 1 << 31 // true
Int32.min != 1 << 31 // also true (!)
cc @scanon
7 Likes
tera
6
Thank you. A few surprises here:
- that different comparison ops choose different paths (heterogeneous vs homogeneous)
- that one of the paths (or both?!) contains a bug
- personally I am surprised that comparison ops between different types are even allowed (I still can't do that, say, with an addition / subtraction, and inconsistencies always strike me odd).
1 Like
dnadoba
(David Nadoba)
7
Looks like we are hitting the heterogenous overloads defined in [SE-104] Protocol-oriented integers. The implementation can be found here.
1 Like
xwu
(Xiaodi Wu)
8
What you describe in the first bullet point is the bug.
@dnadoba linked to the relevant proposal. Comparison and other operators which support heterogeneous types can be expressed without implicit integer promotion: addition and subtraction cannot.
1 Like
scanon
(Steve Canon)
9
Right, to expand on what Xiaodi said, the bug is in the type assigned to expression 1 << 31. The actual comparisons are fine.
Without doing any real investigation, I expect that there's a same-type overload for == on each concrete integer type, but not for !=, so it gets the heterogeneous definition on BinaryInteger, which leads to 1 << 31 having type Int, and on a 64b platform that is (correctly!) not equal to Int32.min. This is quite easy to fix ("simply" add an overload for != as well), but we should be pretty careful about introducing this change, as it can change the behavior of existing programs in very subtle ways.
It's worth noting that it would probably be genuinely useful to provide explicitly typed heterogeneous arithmetic as functions rather than operators, because sometimes you really do need to add a T1 and T2 to produce a T3, and rolling it yourself is a notorious source of bugs in most low-level languages. Writing T3(a) + T3(b) in Swift is safe (it cannot produce an incorrect result value), but will be conservative in its checking (one of the conversions to T3 might fail even though the final result would have been representable).
This is fairly niche however, and probably rightly goes in numerics or a low-level support library, rather than the stdlib.
3 Likes
xwu
(Xiaodi Wu)
10
As I surmised above, I can ascertain that this is a regression in Swift 5 after some spelunking using godbolt.org. @moiseev added the original overloads and wouldn't have just missed one, and my expectation is that (at some point) one was removed inadvertently (or altered) without adequate testing.
Input:
let x = Int32.min == 1 << 31
let y = Int32.min != 1 << 31
let z = !(Int32.min == 1 << 31)
print(x, y, z)
Output (x86-64 swiftc 5.0, -O):
main:
push rbp
mov rbp, rsp
push rbx
push rax
mov byte ptr [rip + (output.x : Swift.Bool)], 1
mov byte ptr [rip + (output.y : Swift.Bool)], 1
mov byte ptr [rip + (output.z : Swift.Bool)], 0
; ...
Output (x86-64 swiftc 4.2, -O):
main:
push rbp
mov rbp, rsp
push r15
push r14
push r12
push rbx
mov byte ptr [rip + (output.x : Swift.Bool)], 1
mov byte ptr [rip + (output.y : Swift.Bool)], 0
mov byte ptr [rip + (output.z : Swift.Bool)], 0
; ...
4 Likes
tera
11
Is this correct?
let a = Int32.max < (1 << 31) // false
let b = Int32.max < Int(1 << 31) // true
let c = Int32.max > (1 << 31) // true
let d = Int32.max > Int(1 << 31) // false
let e = Int32.min < (1 << 31) // false
let f = Int32.min < Int(1 << 31) // true
let g = Int32.min > (1 << 31) // false
let h = Int32.min > Int(1 << 31) // false
print(a, b, c, d, e, f, g, h)
I can see how we land there, i.e. 1 << 31 being treated as Int32.min when treated as Int32 so not sure if this is a bug or a feature. Also:
let a = Int32.max < (1 << 32) // false
let b = Int32.max < Int(1 << 32) // true
let c = Int32.max > (1 << 32) // true
let d = Int32.max > Int(1 << 32) // false
scanon
(Steve Canon)
13
Yes, all of these fall out from the fact that (1 << 31), when used in a comparison with Int32, is inferred to have type Int32 and value –2³¹, and (1 << 32) has type Int32 and value 0 in the same context.
2 Likes
jcavar
(Josip Cavar)
15
Ok, so a bug stdlib makes the compiler choose heterogenous implementation of != on BinaryInteger which requires dynamic dispatch and PWT lookup?
Since the implementation is not known at compile time, @_noLocks correctly rejects this code? Wow, that's so nice!
4 Likes
scanon
(Steve Canon)
16
Pretty much. You ought to be able to work around it in the short term by using !(a == b) instead of a != b (or by explicitly providing a type on both sides of the inequality).
2 Likes