Fastest way to get (const) pointer to struct for inter-operability with C/C++

// Bridging C header file:
struct Cxx_Data {
    double value, rate;
    ... /* a bunch more doubles */
};
double some_c_function(CxxData const* data);

// Swift code:
public struct RT {
     public func analyze() -> Double {
         // I would like to pass the address of actual_data here.
        // And I would *really* like to not gratuitously copy actual_data either!
        some_c_function( ? )
    }

   var actual_data: Cxx_Data

}

  1. If I was willing to make analyze() mutating, I could write
    some_c_function(&actual_data)
    which is quite fast, but at the cost of marking this function mutating, when it's not.
    (And which forces me to make instances of RT be var when the could be let.)

  2. I could use withUnsafePointer { }, but this (as far as I can tell) will make a copy
    of actual_data. I can't use the inout version of withUnsafePointer without making
    the function mutating.

Is there no way to obtain a lightweight pointer to actual_data, given that
my C method specifically says it takes a const pointer?

It does not make a copy, as far as I know. According to the documentation, it

Invokes the given closure with a pointer to the given argument.

Compare also Allow let-to-pointer conversions, where it is said that

Therefore

let result = withUnsafePointer(to: actual_data, { some_c_function($0)} )

should be what you are looking for.

My member variable is a var, not a let. If it was a let, then yes, the new allow let to pointer conversion would apply. But with a var, it appears to make a copy (my timing tests bear this out) unless I pass it with an &, which requires marking the function as mutating.

Note that withUnsafePointer has two forms; one with an inout argument, and one without. The one without may or may not copy, and like I said, it only appears not to in the let case.

Addendum: Looked at the assembly. Even with let, it still makes a copy of the struct.

2 Likes

You are right, I can confirm your observation. I wonder if that is intended or a bug.

Neither form should copy if the value is already in memory at the time you use withUnsafe*Pointer. foo(&x) and withUnsafePointer(to: &x) { foo($0) } are semantically equivalent. A let value could be in registers (or on-stack spill from an LLVM virtual register) and have to be copied to a temporary buffer.

At least in a trivial test case, I'm not able to reproduce the copying behavior:

If you could share a complete example program where you're seeing unexpected copying, we can take a look.

I see absolutely no reason why, in the let case, it would be necessary to make a copy of the struct and issue a pointer against that copy. (Unless of course the author of the compiler is just paranoid and assumes that anyone who has an UnsafePointer can’t be trusted, and is going to turn it into an UnsafeMutablePointer and write over the data…)

I'll post an example, but the case is I have is a class/struct X, with a member variable data that is a struct, and I want the address of self.data from inside a member function of X.

Hmn, are you saying that there's not even a guarantee that self.data is actually in memory, it might exist only in registers because it was being passed on the stack someplace, as part of a function call?

No, there's no reason to copy in either case, if the argument refers to a variable in memory already. The semantics of withUnsafePointer are intended to be strict enough that it shouldn't be necessary to defensively copy. It'd be helpful to see an example of where it is copying so we can investigate why. It's possible that, if it's a member property of a class, there's some bad interaction with the exclusive access checking we do on class properties.

I’ll come up with a standalone example shortly. Should I just post the entirety of the code or make a project available or what?

If it's small enough to fit in a single file, posting it inline here or in a gist is fine. Thanks!

I am not sure if this demonstrates the problem precisely, but here it seems (to me) that withUnsafePointer makes a copy, whereas withUnsafeMutablePointer does not:

class Foo {
    var a = 12345
}

let foo = Foo()
print(Unmanaged.passUnretained(foo).toOpaque())     // 0x0000000101a40c00
withUnsafeMutablePointer(to: &foo.a, { print($0) }) // 0x0000000101a40c10
withUnsafePointer(to: foo.a, { print($0) })         // 0x00007ffeefbff4e8
1 Like

It looks like the inout variation of withUnsafePointer(to: &foo.a) also avoids the copy. The optimizer is probably not smart enough to avoid the lvalue-to-rvalue copy in the last call and pass the mutable variable directly since there are no interfering mutations. @Michael_Gottesman is this something we could conceivably do?

This datastructure and function declaration live in a .h file:

typedef struct CxxData {
    double x, y;
    double m[16];
} CxxData;

double analyze(CxxData const* data);

Here is the swift code:

 struct RT {
     public init() {
            data = CxxData(x: 0, y: 0,   m: (0, 0, 0, 0,    0, 0, 0, 0,    0, 0, 0, 0,    0, 0, 0, 0))
            data2 = CxxData(x: 0, y: 0,  m: (0, 0, 0, 0,   0, 0, 0, 0,    0, 0, 0, 0,   0, 0, 0, 0))
    }
    
    public mutating func really_fast() -> Double {
        // the code for this is what I want/expect, but at the price
        // of making this function be mutating, which is bad
        return analyze(&self.data)
    }

    public func should_be_as_fast() -> Double {
        // I believe that the code here is doing a memcpy.
        // I would love for it not to though.
        return withUnsafePointer(to: self.data2) {
            return analyze($0)
        }
    }
    
    var data: CxxData
    let data2: CxxData
}

Test code (e.g. embed in some app or something):
var rt = RT()
print(rt.really_fast())
print(rt.should_be_as_fast())

If you make a library out of this (just make the analyze() function return some constant, it need not actually look at CxxData) and call both really_fast() and should_be_as_fast() and stop in the xcode debugger with assembly turned on, you can see where a call to memcpy (appears) to be taking place. Calling this routine a few million times and timing the results agrees with the assumption that memcpy is called in the should_be_as_fast() but not in really_fast().

1 Like

Just to sanity check, you are running these tests in an optimized build? Without optimizations on, you might see a substantial difference in the two.

I set the build to “Release” and I see an -O flag for the compilation.

Yeah, so this case looks like a phase ordering issue in the Swift compiler. In SIL, since should_be_as_fast is non-mutating, the function will be modeled as taking self by value as if in a register, and as part of the withUnsafePointer call a stack buffer is allocated and the value is copied into it. It's only later right before we lower to LLVM IR that we see, due to the size of the RT struct, it's really passed by reference in the machine calling convention, so we convert the virtual register into a separate memory location, but we don't subsequently coalesce the memory with other temporary allocations that we formed in the original SIL to hold the value.

@Michael_Gottesman Is this something the large loadable types pass could conceivably do? Maybe we could look for alloc_stack s that are dominated by a definition we're making indirect and initialized to the same value and combine them together.

Generally we'll be eliminating more copies by avoiding in-memory values, but this is a case where the UnsafePointer conversion always induces a memory location. That creates a mismatch with the self value even though it will eventually end up in memory anyway.

To remove the copy created by the apparent calling convention mismatch, an IRGen-time pass needs to

  • rediscover the fact that self is immutable (no problem)
  • reason about the UnsafePointer conversion to prove that it is also immutable (a little messy)
  • do a copy-forwarding style removal/rewrite of the copy_addr (depends on how general you make it)
1 Like

Reading SE-0205 again, this is to be expected:

Optimized Swift code ought to in principle avoid copying when withUnsafePointer(to: x) is passed a stored let property x that is known to be immutable, and with moveonly types it would be possible to statically guarantee this, but in Swift today this cannot be guaranteed.