[Pitch] InlineArray type sugar

I was initially on team no-special-syntax for this, because Set is used far more often than InlineArray would be.

However, I feel like InlineArray’s use cases are quite distinct from Set, so Set’s precedence shouldn’t really apply here.

For example, to turn a [Int] into a Set is just four characters and a whitespace. To turn it into an InlineArray isn’t that much more difficult.

let numbers1 = [1,2,3]
let numbers2: Set = [1,2,3]
let numbers3: InlineArray<3, Int> = [1,2,3]

However, the case that differentiates them is arrays-within-arrays. This is a case that I don't think is anywhere remotely near common to do with a set, but it's likely to be somewhat regular with InlineArrays for matrices. This syntax is ludicrous:

let matrix1 = // matrix
let matrix2: InlineArray<3, InlineArray<3, Int>> = // matrix

There is no way around typing it all out (that i’ve seen - i’m not sure if type inference handles that), only reducing the times you need to type it out via typealias. Typealias doesn’t let you pass parameters, so if you need matrices of many sizes, you need to create one alias for EACH. I’m imagining files filled with dozens of repetitive typealias definitions - I feel like this is something we should avoid and that a syntax sugar could address.

4 Likes

The most serious issue for me is that Array and InlineArray behave so differently with regard to copies. I believe the plan is to also add a COW Array type with fixed size (is that still the plan?) and that type could be a better fit for this kind of sugar.

// Regular COW Array. Copies are cheap, because storage copies happen on mutation.

let a: [Int] = [....] 

// Inline Array. Copies are more expensive, because the elements are stored inline.

let b: [5 x Int] = [....]

This difference could be particularly confusing if we do adopt something like a value syntax:

// Regular COW Array, I assume?

let a = [5 x someValue]

// Inline Array.

let b: [5 x ValueType] = [5 x someValue]
2 Likes

(reply was written to an earlier version of Karl's comment, which he has since clarified):

Ben is saying that they wouldn’t be replaced by InlineArray. For values, the syntax would be just a way to spell Array literals with a repeating element (which is a potential source of confusion, but not hazardous in the specific direction that you’re worried about).

3 Likes

Yeah I didn't word that very well; I've edited it to be a bit clearer.

1 Like

This can be spelled let numbers3: InlineArray = [1, 2, 3].

This can be spelled

let matrix2: InlineArray = [
  [...] as InlineArray,
  [...],
]
1 Like

A heap-allocated fixed-size COW array is very niche, and of almost no utility compared to other array variants. An InlineArray can be allocated on the stack or inline, a COW array requires a heap allocation. InlineArray (or a NoncopyableArray) requires no uniqueness checks on write, whereas a COW array would.

Really the only benefit of a COW-but-fixed-size array is to facilitate compile-time micro optimizations based on its fixed size (e.g. potentially eliminate more bounds checks, autovectorize operations more efficiently). The times when these things are critical but you also want to make copies and are not mutating the array, just reading it (so not going to be hit by the uniqueness check cost), are likely vanishingly small.

In the rare cases you might need this, it's probably better to synthesize it from other things the standard library ought to have in future, such as noncopyable and COW box types that could hold an InlineArray.

Given this, the idea that we would privilege that type, and not InlineArray, with this sugar doesn't seem to hold up.

4 Likes

Also:

let x: InlineArray<_, InlineArray> = [
    [1, 0],
    [0, 1]
]
2 Likes

In all these cases though it seems fairly clear that the sugared case would be nicer while also allowing you to be explicit about the count if you prefer, which I do think can have readability benefit without coming at the readability cost of having to spell out the nested type verbosely. The ease of specifying an explicit count also allows you to have the compiler check you got the count on the literal right.

// if you want these to be doubles not ints, this is also much 
// more neatly done with this syntax than the full type name and <> 
let identity: [2 x [2 x Double]] = [
    [1, 0],
    [0, 1],
]

And if you want full inference, it also looks clearer IMO

let identity: InlineArray<_, InlineArray> =
// vs
let identity: [_ x [_ x _] =
6 Likes

I'd take it a step further: anyone who's writing anything close to InlineArray<_, InlineArray<_, Elem>> is already putting themselves into a needlessly contrived situation, because they should be defining a typealias to pass that thing around (or wrapping it in a type that adds appropriate operations). So there's no need to optimize the fully spelled out version.

typealias Matrix<let R: Int, let C: Int, E> = InlineArray<R, InlineArray<C, E>>

let matrix2: Matrix = [
  [1, 0],
  [0, 1],
]
5 Likes

This often doesn't work out in practice though. I've often found myself defining type aliases and then regretting it, and it would likely happen here too.

I am looking at a project just now that has adopted InlineArray (it has about 100 declarations of it and would benefit greatly from this sugar). It declares various matrices, but they end up varying by size (8, 16, 32) and element type (Float32, UInt16). So you end up making Matrix a generic type alias and now you don't really have the same benefit. In practice probably a type alias is the wrong thing here and what you want is a Matrix that aggregates an InlineArray and provides higher level operations.

7 Likes

Right, and even in that case, it's not so bad if the syntax for nested inline arrays is "ludicrous", because encapsulating it means it's written only once (or very few times), internally, and doesn't (shouldn't) leak out to the rest of the clients.

FWIW, I'm not stating this as an argument either for or against better sugar. I'd make the same case if someone was writing [2 x [2 x Int]] throughout their code instead of wrapping it in some other type. I would imagine that the operations available on the bare inline array don't get one very far, and real usage (at least for these nested situations) practically begs for their own type that you can hang the operations off of.

1 Like

But sometimes you don't want to go to any of this trouble, and it's better to just open code things. From my test to see if InlineArray improved the execution time of the nbody problem benchmark:

struct System {
#if INLINEARRAY
    var mass:     [5 x Precision]
    var position: [5 x SIMD4<Precision>]
    var velocity: [5 x SIMD4<Precision>]
    var force:    [10 x SIMD4<Precision>] = .init(repeating: .zero)
#else
    var mass:     [Precision]
    var position: [SIMD4<Precision>]
    var velocity: [SIMD4<Precision>]
    var force:    [SIMD4<Precision>] = .init(repeating: .zero, count: N)
#endif

I was pretty pleased to be able to replace the InlineArray code here, aesthetically.

nbody benchmark for those interested

Adopting InlineArray roughly halves the computation time.

typealias Precision = Double

let SOLAR_MASS: Precision = 4.0 * .pi * .pi
let DAYS_PER_YEAR: Precision = 365.24
let N_BODIES: Int = 5
let N: Int = N_BODIES * (N_BODIES - 1) / 2
let dt: Precision = 0.01

@available(macOS 9999,*)
struct System {
#if INLINEARRAY
    var mass: [5   x Precision]
    var position: [5 x SIMD4<Precision>]
    var velocity: [5 x SIMD4<Precision>]
    var force: [10 x SIMD4<Precision>] = .init(repeating: .zero)
#else
    var mass: [Precision]
    var position: [SIMD4<Precision>]
    var velocity: [SIMD4<Precision>]
    var force: [SIMD4<Precision>] = .init(repeating: .zero, count: N)
#endif

    init() {
        position = [
            .init(0,0,0,0),
            .init(4.8414314424647209,-1.16032004402742839,-0.103622044471123109,0),
            .init(8.34336671824457987,  4.12479856412430479, -4.03523417114321381e-01,0),
            .init(1.28943695621391310e+01, -1.51111514016986312e+01, -2.23307578892655734e-01,0),
            .init(1.53796971148509165e+01, -2.59193146099879641e+01,  1.79258772950371181e-01,0),
        ]
    
        velocity = [
            .init(0,0,0,0),
            .init(1.66007664274403694e-03,7.69901118419740425e-03,-6.90460016972063023e-05,0) * DAYS_PER_YEAR,
            .init(-2.76742510726862411e-03,4.99852801234917238e-03,2.30417297573763929e-05,0) * DAYS_PER_YEAR,
            .init(2.96460137564761618e-03,2.37847173959480950e-03,-2.96589568540237556e-05,0) * DAYS_PER_YEAR,
            .init(2.68067772490389322e-03,1.62824170038242295e-03,-9.51592254519715870e-05,0) * DAYS_PER_YEAR,
        ]
    
        mass = [
            SOLAR_MASS,
            9.54791938424326609e-04 * SOLAR_MASS,
            2.85885980666130812e-04 * SOLAR_MASS,
            4.36624404335156298e-05 * SOLAR_MASS,
            5.15138902046611451e-05 * SOLAR_MASS,
        ]
        
        velocity[0] = -(0..<N_BODIES).reduce(into: .zero) { p, i in
            p += velocity[i] * mass[i]
        } / SOLAR_MASS
    }

    mutating func advance() {
        var i = 0
        for j in 0..<N_BODIES {
            for k in j&+1..<N_BODIES {
                let d = position[j] - position[k]
                let d2 = (d*d).sum()
                let m = dt / (d2 * d2.squareRoot())
                force[i] = d * m

                i &+= 1
            }
        }
        
        i = 0
        for j in 0..<N_BODIES {
            for k in j&+1..<N_BODIES {
                let f = force[i]
                velocity[j] -= f * mass[k]
                velocity[k] += f * mass[j]

                i &+= 1
            }
        }
        
        for j in 0..<N_BODIES {
            position[j] += velocity[j] * dt
        }
    }

    func energy() -> Precision {
        var e: Precision = 0.0
        for i in 0..<N_BODIES {
            let m = mass[i]
            let v = velocity[i]
            e += (v*v).sum() * m * 0.5
            for j in i&+1..<N_BODIES {
                let d = position[i] - position[j]
                let dist = (d*d).sum().squareRoot()
                e -= (m * mass[j]) / dist
            }
        }
        return e
    }
}

@available(macOS 9999,*)
func nbody() {
    let count =
        if CommandLine.argc > 1 {
            Int(CommandLine.arguments[1]) ?? 50000000
        } else {
            50000000
        }

    var system = System()
    let initial = system.energy()
    for _ in 0..<count {
        system.advance()
    }
    let final = system.energy()

    print(initial)
    print(final)
}

@main
struct Main {
  static func main() {
    if #available(macOS 9999,*) {
      
      var timings: [Duration] = []
      
      for _ in 0..<5 {
        let timing = ContinuousClock().measure {
          nbody()
        }
        timings.append(timing)
      }
      print(timings.min()!)
    }
}
2 Likes

This becomes more pronounced when dealing with multiple dimensions:

let fiveByFive: InlineArray<5, InlineArray<5, Int>> = .init(repeating: .init(repeating: 99))

maybe it would be better to introduce separate generic type like Matrix?
Matrix operations could lead to use matrix extension of ISA(Instruction Set Architecture)

Swift supports generic type aliases so you could in fact define a Matrix type alias that is generic over the count:

typealias Matrix<let count: Int, E> = InlineArray<count, InlineArray<count, E>>
2 Likes

2D array can be viewed as matrix, but what about tensors?
maybe introduce Tensor variadic generic type with variable number of generic parameters?

I’m going to push back on this one.

The character sequence “[_ x [_ x _]]” is entirely opaque to the reader. It gives no indication what it means, and just looks like an esoteric incantation.

Imagine someone new to a codebase reading it for the first time. Even if they are an experienced Swift programmer, even if they already know about inline arrays, that is a jumble of gibberish.

Also you left out a square bracket.

13 Likes

I have been following this topic since Vector/InlineArray was first proposed and I am still not convinced by any of the type syntax sugar above. Especially the syntax using the letter x is very different from what I am used to reading in Swift (or any other language, for that matter). The other sugar proposals don’t feel like they carry their weight either. Same goes for the repetition sugar.

7 Likes

I agree. I am not against it happening though. Clearly people want it. However, 'x' just needs to be disqualified simply because there are going to be lots of variables called 'x'. I can't necessarily say the same for 'of'. I looked through one of my largest projects and couldn't find of used on it's own except in function arguments.

So at the very minimum, I am hoping that 'x' is disqualified and at 'of' takes the lead. I get it 'x' kinda looks right.

Maybe even ofType or 'oftype`, but that seems lengthy.

(5 ofType Int)

I also prefer not using square brackets as there as the case was made quite clearly that parentheses and square brackets would not be needed to disambiguate.

1 Like

I am opposed to the proposed syntax (and 95% of the proposed syntax by others) and think the same as Rauhul.

As an early adopter of InlineArray in my networking library, fully typing out InlineArray<X, Y> is not that problematic, exhausting or verbose to me. As mentioned in this thread, using typealiases can help flexibility if you need/want that.

4 Likes

I’m also for the idea of not (yet) introducing new syntax sugar.

3 Likes