@_noLocks with Int32 inequality check

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

Seems to work fine for me on godbolt (make sure you use -experimental-performance-annotations). Have you tried !(x == 1)?

2 Likes

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.

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

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

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

Looks like we are hitting the heterogenous overloads defined in [SE-104] Protocol-oriented integers. The implementation can be found here.

1 Like

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

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

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

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

Yes

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

Issued filed as #70041.

2 Likes

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

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