Code generation bug in release mode - Xcode Version 16.0 (16A242d)

Hello Swift community,

I know that you should never blame the compiler for your own code, but...
With one of my small test programs I have a snippet which works correctly in Debug Builds but not in Release Builds.

if end != start {
    let startMinusOne = start.advanced(by: -1)
    startMinusOne.pointee = start.pointee
    //print(startMinusOne.pointee) included in last version
    start.pointee = 0x2E 
    start = startMinusOne
}

Both start and end are UnsafeMutablePointer.
This code produces the following assembler code (ARM64):

->  0x100003160 <+284>:  cmp    x22, x9                   ; x22 = end, x9 = start
    0x100003164 <+288>:  b.eq   0x100003174               ; <+304> at Decimal64.swift
    0x100003168 <+292>:  mov    w8, #0x2e                 ; =46 
    0x10000316c <+296>:  strb   w8, [x9], #-0x1
    0x100003170 <+300>:  mov    x23, x9

The Swift code line "startMinusOne.pointee = start.pointee" is clearly missing in this assembler code. If I comment this line out, I get the exact same assembler code. The Debug Build assembler is much longer, but produces a correct result.

Is this a bug in the optimizer or am I doing something wrong?

Kind regards,
Dirk

BTW: If un-comment the print statement everything works as expected.

I haven't worked it through (in part because your example is not self-contained), but it smells like you are using unsafe pointers to violate the Law of Exclusivity. Swift would otherwise forbid violations, but since you're using unsafe APIs, "it is the programmer's responsibility to follow the rule"; failure to do so leads to nasal demons.

2 Likes

I tried to simplify it as much as possible and got an example which works in debug mode but not in release mode:

func fill(_ end: UnsafeMutablePointer<UInt8>) -> UnsafeMutablePointer<UInt8> {
    var start = end
    let count = Int.random(in: 0..<17)
    print(count)
    var i = 0
    while i < count {
        start -= 1
        i += 1
        start.pointee = UInt8(i)
    }
    return start
}

func insertDot(_ start: UnsafeMutablePointer<UInt8>, _ end: UnsafeMutablePointer<UInt8>) -> UnsafeMutablePointer<UInt8> {
    var start = start
    if (start != end) {
        let startMinusOne = start.advanced(by: -1)
        startMinusOne.pointee = start.pointee
        start.pointee = 0x2E // .
        start -= 1
    }
    return start
}

// this is needed in the "real" application and can't be an allocated array
var data: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
           UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
           UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
           UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
) = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)

withUnsafeMutablePointer(to: &data.30) { end in
    var start = fill(end)
    start = insertDot(start, end)
    print(start.pointee)
}
print(data)

This will print the same random number two times in debug mode. In release mode the second number is always 0.

That doesn’t look like valid code to me. The issue is this:

withUnsafeMutablePointer(to: &data.30) { end in
    …
}

If you take a pointer to a single element of a tuple, the compiler only guarantees access to that element. You can’t then navigate around inside the tuple. For example, the compiler is free to implement the above like so:

var tmp = data.30
withUnsafeMutablePointer(to: &tmp) { end in
    …
}
data.30 = tmp

You should take a pointer to data as a whole, and then navigate around inside that.

This is one of the issues that I describe in The Peril of the Ampersand.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

1 Like

Thanks Quinn.

I tried it in the following way. Still unsuccessful.


withUnsafeMutablePointer(to: &data) { dataptr in
    dataptr.withMemoryRebound(to: UInt8.self, capacity: 40) { first in
        var end = first + 30
        var start = fill(end)
        start = insertDot(start, end)
        print(start.pointee)
    }
}
print(data)

This works in debug mode or with an additional print statement inside insertDot but not in release mode.
What can I do?

Completely omitting the store definitely seems like a bug (and a regression from 5.10 to boot). Even just looking at the generated code for insertDot shows that it's not getting emitted, probably because something is assuming -1 is automatically out of bounds. But it definitely isn't out of bounds here!

2 Likes

Thanks @jrose. That means the compiler doesn‘t care how I got the pointer in the first place and will make my search for a workaround even harder.

I tried to move the „if start != end“ out of the insertDot function and thereby removing the parameter end completely (which could have been a violation of the „Law of exclusivity“ if I used both to access/modify something). Still no luck.
(BTW: Thanks for the „Swift regrets“ series)

1 Like

It works if you make fill and insertDot methods on UnsafeMutableRawBufferPointer and then use withUnsafeMutableBytes(of: &data)

extension UnsafeMutableRawBufferPointer {
  func fill(_ end: Int) -> Int {
    var start = end
    let count = Int.random(in: 0..<17)
    print(count)
    for i in 0..<count {
      start &-= 1
      self[start] = UInt8(i + 1)
    }
    return start
  }
  func insertDot(_ start: Int, _ end: Int) -> Int {
    var start = start
    if start != end {
      let i = start &- 1
      self[i] = self[start]
      self[start] = 0x2E  // .
      start = i
    }
    return start
  }
}

do {
    // this is needed in the "real" application and can't be an allocated array
    var data: (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
               UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
               UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
               UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
    ) = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)

    withUnsafeMutableBytes(of: &data) { bytes in
        let end = 30
        var start = bytes.fill(end)
        start = bytes.insertDot(start, end)
        print(bytes[start])
    }
    print(data)
}
1 Like

Thank you very much. This will generate some additional code for the boundary checks and exclusive access. The former can be removed by using UnsafeMutableRawPointers instead of the buffer version.
It is at least a working version in release mode.
It is still a compiler error IMHO, that the previous version of insertDot can be compiled on its own where the compiler can't know in advance if "somePointer - 1" is legal and the result is incorrect code without any warnings.