Strange TaskLocal behaviour

The following minimal app:

struct Foo: Equatable {
    var value = 0
    @TaskLocal static var state: UnsafeMutablePointer<Foo>?
}

func test() {
    var state = Foo()
    withUnsafeMutablePointer(to: &state) { state1 in
        Foo.$state.withValue(state1) {
            let state2 = Foo.state!
            precondition(state1.pointee.value == state2.pointee.value)
            
            state1.pointee.value = 1
            state2.pointee.value = 2
            precondition(state1.pointee.value == state2.pointee.value) // ASSERT FAILED IN -O
//            precondition(state1 == state2) // BUT NOT IF YOU UNCOMMENT THIS LINE!
        }
    }
}

test()

works fine in Debug but fails on precondition in release. Unless I uncomment the seemingly irrelevant line! Is this a bug or am I using it wrong?

2 Likes

A simplified version:

struct Foo {
    var value = 0
    @TaskLocal static var state: UnsafeMutablePointer<Foo>?
}

func test() {
    var state = Foo()
    withUnsafeMutablePointer(to: &state) { statePtr in
        Foo.$state.withValue(statePtr) {
            Foo.state!.pointee.value = 1
        }
    }
    precondition(state.value == 1)
}

test()

Same issue: works in debug, fails on precondition in release.

In real code the assignment happens deeper, in a place where statePtr is not available (otherwise I could have done merely "statePtr.pointee.value = 1" (and would not need the TaskLocal storage to begin with)). As a workaround I can make Foo a class :unamused:

1 Like

Thanks for reporting -- this rather looks like a bug with optimizations than something the task local could be breaking.

We'll look into it -- filing an issue would be nice: GitHub - apple/swift: The Swift Programming Language

2 Likes

Thank you!

Do you know how expensive are TaskLocals? Specifically "writing to a task local variable" (TaskLocal.withValue(...)) and "reading from a task local variable" (accessing the @TaskLocal variable)?

If in doubt, benchmark your code.

It's a thread-local access, and an allocation in the task-local allocator. That doesn't say much in terms of "is it ok to have in my code?" because it depends how critical your codepath there is, and what your performance goals are etc.

1 Like

Yep, good idea. I figured instead of using TaskLocal.withValue I could simply use an initial value for my local task variable and it will be allocated only when used first time and only once per task, so the TaskLocal.withValue speed is not important:

class Foo {
    var value = 0
    @TaskLocal static var foo = Foo()
}
...
Foo.foo.value = 123

I measured the task local variable access performance on ARM and it gave me ~32 nanoseconds per access, which is not bad at all. Those accesses are used in both "try" and "throw" of my bespoke "unchecked exceptions" implementation (which core is remarkably short - just 25 lines!).

2 Likes

Thanks for reporting!
Should be fixed with EscapeUtils: fix escape result in case an address is stored by eeckstein ยท Pull Request #71554 ยท apple/swift ยท GitHub

2 Likes