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")
}