3

I have a fixed size multi-dimensional array of numbers (floats, usually, but ints in my example code to avoid being distracted by conversion overheads), and I want to manipulate it efficiently. Swift doesn't provide multi-dimensional arrays as such, but you can get the effect through an array of 1D arrays. However, these seem to be very, very slow. Is there a better way?

I have a test problem (which I've used to benchmark other languages) where I pass two 2D arrays to a subroutine which sets each element of one to the corresponding element of the other plus the sum of the two index values. (Which means what happens to each element depends on its coordinates, which is what happens in most real-world cases.)

I've compiled using the -Ounchecked flag.

Option 1: Using an array of 1D arrays, I get very slow performance. 10 passes took 1.5 seconds.

Option 2: Using the rather neat idea in http://blog.trolieb.com/trouble-multidimensional-arrays-swift where an Array2D class uses an underlying 1D array and implements subscript() to make it look like a 2D array, things speed up a lot (2 orders of magnitude): 1000 passes took 1.0 seconds

Option 3: Falling back on the very awkward sort of code one used to use in C, where you use a 1D array and do the index = (row * columns) + column calculation explicitly, things speed up yet again (not quite 2 orders of magnitude) 100000 passes took 3.6 seconds.

Option 3 is within a factor 2 of what I get out of the equivalent C code compiled with -O3 in clang, so is fine for a early-days compiler. The problem is that it's really ugly, awkward and error prone. There are tricks one can use in C, like allocating arrays of pointers to the start of each line (Numerical Recipes in C does this) to allow you to use a 2D syntax for arrays, and with an object-oriented C you can make this quite elegant as well as efficient. My question is really is there a way in Swift to get code like array[Iy][Ix] (or array[Iy,Ix] or whatever, as opposed to array[Iy*Ny + Ix]) to run fast?

I should say that I'm very new to Swift, and I like what I've seen so far, and I appreciate that compilers will only get faster. I do a lot of coding on scientific applications using fixed size multi-dimensional arrays, and I'm interested in the possibility of using Swift some time in the future. Or should I be asking Apple to add real multi-dimensional array support to Swift?

Here's the test code I've been using:

//
//  main.swift
//
//  Tests 3 ways of handling 2D arrays in Swift. Test takes a 2D array and calls a routine
//  that takes each element of an input array and adds the X and Y index values to it and
//  returns an array with the result.
//
//  Command line arguments: Option Nrpt Nx Ny
//
//  Option is type of array used (1: Swift array of arrays, 
//                                2: Array2D 1D array looking like a 2D array
//                                3: 1D array used like a 2D array with explicit index calculation)
//  Nrpt is number of repeats of subroutine call
//  Nx, Ny are array dimensions.
//

import Darwin

//  Array2D comes from http://blog.trolieb.com/trouble-multidimensional-arrays-swift/

class Array2D {
    var cols:Int, rows:Int
    var matrix: [Int]

    init(cols:Int, rows:Int) {
        self.cols = cols
        self.rows = rows
        matrix = Array(count:cols*rows, repeatedValue:0)
    }
    subscript(col:Int, row:Int) -> Int {
        get { return matrix[cols * row + col] }
        set { matrix[cols*row+col] = newValue }
    }
    func colCount() -> Int { return self.cols }
    func rowCount() -> Int { return self.rows }
}

//  Using a 'proper' Swift '2D' array - ie an array of 1D arrays
func Subr (Input: Array<Array<Int>>, Nx: Int, Ny : Int, inout Output: Array<Array<Int>>) {
    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            Output[Iy][Ix] = Input[Iy][Ix] + (Ix + Iy)
        }
    }
}

//  Using an Array2D array - wrapping up a 1D array to act as a 2D one.
func Subr2d (Input: Array2D, Nx: Int, Ny : Int, inout Output: Array2D) {
    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            Output[Ix,Iy] = Input[Ix,Iy] + (Ix + Iy)
        }
    }
}

//  Using a 1D Swift array and doing the indexing explicitly
func Subr1d (Input: [Int], Nx: Int, Ny: Int, inout Output: [Int]) {
    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            Output[Iy * Nx + Ix] = Input[Iy * Nx + Ix] + (Ix + Iy)
        }
    }
}

var Option:Int = 1
if let argStr = String.fromCString(C_ARGV[1]) {
    if let argInt = argStr.toInt() { Option = argInt }
}

var Nrpt:Int = 100
if let argStr = String.fromCString(C_ARGV[2]) {
    if let argInt = argStr.toInt() { Nrpt = argInt }
}

var Nx:Int = 2000;
if let argStr = String.fromCString(C_ARGV[3]) {
    if let argInt = argStr.toInt() { Nx = argInt }
}

var Ny:Int = 10;
if let argStr = String.fromCString(C_ARGV[4]) {
    if let argInt = argStr.toInt() { Ny = argInt }
}


println("Repeats: \(Nrpt), Array \(Nx) by \(Ny)")

switch Option {
case 1:

    println ("Using an ordinary Swift '2D' array of arrays")

    var array = Array(count:Ny, repeatedValue:Array(count:Nx, repeatedValue:Int()))

    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            array[Iy][Ix] = (Ix + Iy)
        }
    }

    var output = Array(count:Ny, repeatedValue:Array(count:Nx, repeatedValue:Int()))

    let start : UInt64 = mach_absolute_time()

    for Irpt in 0...Nrpt-1 {
       Subr(array,Nx,Ny,&output)
    }

    let duration : UInt64 = mach_absolute_time() - start

    check:
    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            let Expected = array[Iy][Ix] + (Ix + Iy)
            if (output[Iy][Ix] != Expected) {
                println("Error at \(Ix),\(Iy) Got \(output[Iy][Ix]) expected \(Expected)")
                break check
            }
        }
    }

    var info : mach_timebase_info = mach_timebase_info(numer: 0, denom: 0)
    mach_timebase_info(&info)

    let total = (duration * UInt64(info.numer) / UInt64(info.denom)) / 1_000_000
    println("2D array took:\(total) ms.")

case 2:

    println ("Using the Array2D class")

    var array2 = Array2D(cols: Nx, rows: Ny)
    var output2 = Array2D(cols: Nx, rows: Ny)

    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            array2[Ix,Iy] = (Ix + Iy)
        }
    }

    println("Timing array2D version")

    let start2 : UInt64 = mach_absolute_time()

    for Irpt in 0...Nrpt-1 {
        Subr2d(array2,Nx,Ny,&output2)
    }

    let duration2 : UInt64 = mach_absolute_time() - start2

    check:
    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            let Expected = array2[Ix,Iy] + (Ix + Iy)
            if (output2[Ix,Iy] != Expected) {
                println("Error at \(Ix),\(Iy) Got \(output2[Ix,Iy]) expected \(Expected)")
                break check
            }
        }
    }


    var info2 : mach_timebase_info = mach_timebase_info(numer: 0, denom: 0)
    mach_timebase_info(&info2)

    let total2 = (duration2 * UInt64(info2.numer) / UInt64(info2.denom)) / 1_000_000
    println("Array2D version took:\(total2) ms.")

case 3:

    println ("Using an a 1D array and handling the indexing explicitly")

    var array3 = Array(count:Ny * Nx, repeatedValue:Int())

    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            array3[Iy * Nx + Ix] = (Ix + Iy)
        }
    }

    var output3 = Array(count:Ny * Nx, repeatedValue:Int())

    let start3 : UInt64 = mach_absolute_time()

    for Irpt in 0...Nrpt-1 {
        Subr1d(array3,Nx,Ny,&output3)
    }

    let duration3 : UInt64 = mach_absolute_time() - start3

    check:
    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            let Expected = array3[Iy * Nx + Ix] + (Ix + Iy)
            if (output3[Iy * Nx + Ix] != Expected) {
                println("Error at \(Ix),\(Iy) Got \(output3[Iy * Nx + Ix]) expected \(Expected)")
                break check
            }
        }
    }

    var info3 : mach_timebase_info = mach_timebase_info(numer: 0, denom: 0)
    mach_timebase_info(&info3)

    let total3 = (duration3 * UInt64(info3.numer) / UInt64(info3.denom)) / 1_000_000
    println("1D array took:\(total3) ms.")

default:
    println ("Invalid option code. Must be 1,2, or 3")
}
KeithS
  • 113
  • 1
  • 7
  • What are your optimization settings? Last I checked, Swift is 100% absolutely horrendous when it comes to repeatedly accessing arrays. – nhgrif Nov 26 '14 at 00:44
  • -Ounchecked Without that, it really is slow. – KeithS Nov 26 '14 at 00:46
  • (You should use the cool new Xcode 6 unit test time-profiler feature!) Great stuff; package it up and submit it to Apple. I still have code that has had to remain in Objective-C because the Swift translation is prohibitively slow. Apple _wants_ test cases like this. – matt Nov 26 '14 at 04:29
  • @matt Thanks for the encouragement. It doesn't look as if there's a magic solution in Swift as it stands, so I've packaged this up and submitted it to Apple. We'll see if anything happens... – KeithS Nov 28 '14 at 06:05
  • KeithS I am no expert but I sure found a lot of stuff on devforums complaining about Swift array access (just as @nhgrif says). And I can see posts from the Swift team saying they are working on performance issues. So it can't hurt to submit such a good test case. – matt Nov 28 '14 at 06:13

2 Answers2

1

Chris Lattner himself responded on Apple dev forums about this and sounds like #2/#3 our best solutions until an imminent compiler fix is made .

"This is a known issue: 2D arrays ... can provoke extremely poor performance because the copy-on-write (COW) optimization they are based on is defeated in some cases...

The fix for it just narrowly missed the 6.1 release because it requires some internal infrastructure work. That said, it will go out in the next significant update of the swift compiler.

In the meantime, there are often (ugly but effective) workarounds you can use. For example, if your arrays are rectangular, you can use a single array sized to m*n elements, and manually index into it.

-Chris"

SafeFastExpressive
  • 3,637
  • 2
  • 32
  • 39
1

In a way, Apple have answered my question. I've not looked at this for a while now - and, indeed, haven't even been using Swift. But I've just installed XCode 9 and Swift 4, and thought I'd see if things have changed. I had to make some quick changes to get the test program to build, and I've tried it again.

Bottom line: All three options run in about the same time now, and that speed is not bad at all. I think that's a remarkable improvement, and it means that the standard Swift way of handling a 2D array - as an array of arrays - no longer has a performance penalty, and, at least on the basis of this test, is clearly the way to go now. Which is exactly what I think everyone would want. (I've built with -Ounchecked, which does make about a factor 2 difference.)

Compared to the equivalent code in C++, bearing in mind that you have to go through some hoops to pass multi-dimensional arrays to C++ routines, I think this is now much easier to code in Swift. Last time I said the fastest Swift option (the messy 'do the indexing yourself' option) ran only a factor 2 slower than the equivalent C++. Actually, I now see a factor 4 speed-up from using C++ and clang, but that's because clang now seems to have improved its already impressive optimisation (it throws vector instructions at the problem in a most ingenious way - it did that before, but now it seems to have gotten rid of some additional overheads). That's something I imagine may come to Swift with time; the important thing to my mind is that - again, on the basis of this one test - Swift no longer seems to be restricted by its array handling. Since I originally posted this question, clang/C++ has improved by a factor 2, but Swift has improved out of sight.

Here's the revised code:

//
//  main.swift
//
//  Tests 3 ways of handling 2D arrays in Swift. Test takes a 2D array and calls a routine
//  that takes each element of an input array and adds the X and Y index values to it and
//  returns an array with the result.
//
//  Command line arguments: Option Nrpt Nx Ny
//
//  Option is type of array used (1: Swift array of arrays,
//                                2: Array2D 1D array looking like a 2D array
//                                3: 1D array used like a 2D array with explicit index calculation)
//  Nrpt is number of repeats of subroutine call
//  Nx, Ny are array dimensions.
//

import Foundation

//  Array2D comes from http://blog.trolieb.com/trouble-multidimensional-arrays-swift/

class Array2D {
    var cols:Int, rows:Int
    var matrix: [Int]

    init(cols:Int, rows:Int) {
        self.cols = cols
        self.rows = rows
      matrix = Array(repeating:0, count:cols*rows)
    }
    subscript(col:Int, row:Int) -> Int {
        get { return matrix[cols * row + col] }
        set { matrix[cols*row+col] = newValue }
    }
    func colCount() -> Int { return self.cols }
    func rowCount() -> Int { return self.rows }
}

//  Using a 'proper' Swift '2D' array - ie an array of 1D arrays
func Subr (Input: Array<Array<Int>>, Nx: Int, Ny : Int, Output: inout Array<Array<Int>>) {
    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            Output[Iy][Ix] = Input[Iy][Ix] + (Ix + Iy)
        }
    }
}

//  Using an Array2D array - wrapping up a 1D array to act as a 2D one.
func Subr2d (Input: Array2D, Nx: Int, Ny : Int, Output: inout Array2D) {
    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            Output[Ix,Iy] = Input[Ix,Iy] + (Ix + Iy)
        }
    }
}

//  Using a 1D Swift array and doing the indexing explicitly
func Subr1d (Input: [Int], Nx: Int, Ny: Int, Output: inout [Int]) {
    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            Output[Iy * Nx + Ix] = Input[Iy * Nx + Ix] + (Ix + Iy)
        }
    }
}

var Option:Int = 1
if (CommandLine.argc > 1) {
    let argStr = CommandLine.arguments[1]
    if let argInt = Int(argStr) { Option = argInt }
}

var Nrpt:Int = 100
if (CommandLine.argc > 2) {
    let argStr = CommandLine.arguments[2]
    if let argInt = Int(argStr) { Nrpt = argInt }
}
var Nx:Int = 2000;
if (CommandLine.argc > 3) {
    let argStr = CommandLine.arguments[3]
    if let argInt = Int(argStr) { Nx = argInt }
}

var Ny:Int = 10;
if (CommandLine.argc > 4) {
    let argStr = CommandLine.arguments[4]
    if let argInt = Int(argStr) { Ny = argInt }
}

print("Repeats: \(Nrpt), Array \(Nx) by \(Ny)")

switch Option {
case 1:

    print ("Using an ordinary Swift '2D' array of arrays")

    var array = Array(repeating:Array(repeating:Int(), count:Nx), count:Ny)

    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            array[Iy][Ix] = (Ix + Iy)
        }
    }

    var output = Array(repeating:Array(repeating:Int(), count:Nx), count:Ny)

    let start : UInt64 = mach_absolute_time()

    for _ in 0...Nrpt-1 {
      Subr(Input: array,Nx: Nx,Ny: Ny,Output: &output)
    }

    let duration : UInt64 = mach_absolute_time() - start

    check:
    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            let Expected = array[Iy][Ix] + (Ix + Iy)
            if (output[Iy][Ix] != Expected) {
                print("Error at \(Ix),\(Iy) Got \(output[Iy][Ix]) expected \(Expected)")
                break check
            }
        }
    }

    var info : mach_timebase_info = mach_timebase_info(numer: 0, denom: 0)
    mach_timebase_info(&info)

    let total = (duration * UInt64(info.numer) / UInt64(info.denom)) / 1_000_000
    print("2D array took:\(total) ms.")

case 2:

    print ("Using the Array2D class")

    let array2 = Array2D(cols: Nx, rows: Ny)
    var output2 = Array2D(cols: Nx, rows: Ny)

    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            array2[Ix,Iy] = (Ix + Iy)
        }
    }

    print("Timing array2D version")

    let start2 : UInt64 = mach_absolute_time()

    for _ in 0...Nrpt-1 {
      Subr2d(Input: array2,Nx: Nx,Ny: Ny,Output: &output2)
    }

    let duration2 : UInt64 = mach_absolute_time() - start2

    check:
    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            let Expected = array2[Ix,Iy] + (Ix + Iy)
            if (output2[Ix,Iy] != Expected) {
                print("Error at \(Ix),\(Iy) Got \(output2[Ix,Iy]) expected \(Expected)")
                break check
            }
        }
    }


    var info2 : mach_timebase_info = mach_timebase_info(numer: 0, denom: 0)
    mach_timebase_info(&info2)

    let total2 = (duration2 * UInt64(info2.numer) / UInt64(info2.denom)) / 1_000_000
    print("Array2D version took:\(total2) ms.")

case 3:

    print ("Using an a 1D array and handling the indexing explicitly")

    var array3 = Array(repeating:Int(), count:Ny * Nx)

    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            array3[Iy * Nx + Ix] = (Ix + Iy)
        }
    }

    var output3 = Array(repeating:Int(), count:Ny * Nx)

    let start3 : UInt64 = mach_absolute_time()

    for _ in 0...Nrpt-1 {
      Subr1d(Input: array3,Nx: Nx,Ny: Ny,Output: &output3)
    }

    let duration3 : UInt64 = mach_absolute_time() - start3

    check:
    for Iy in 0...Ny-1 {
        for Ix in 0...Nx-1 {
            let Expected = array3[Iy * Nx + Ix] + (Ix + Iy)
            if (output3[Iy * Nx + Ix] != Expected) {
                print("Error at \(Ix),\(Iy) Got \(output3[Iy * Nx + Ix]) expected \(Expected)")
                break check
            }
        }
    }

    var info3 : mach_timebase_info = mach_timebase_info(numer: 0, denom: 0)
    mach_timebase_info(&info3)

    let total3 = (duration3 * UInt64(info3.numer) / UInt64(info3.denom)) / 1_000_000
    print("1D array took:\(total3) ms.")

default:
    print ("Invalid option code. Must be 1,2, or 3")
}
KeithS
  • 113
  • 1
  • 7