Sparse matrices are matrices that are mostly-empty (i.e. most of their entries are zeros). By realizing a matrix is sparse and using a sparse matrix implementation, we can save space and improve performance, especially when working with large dimensions. Rather than allocating a buffer of `rows * columns`

size, a sparse matrix implementation will proportionally grow in size with the number of non-zero entries in the matrix.

In iOS 9 and macOS 10.11, Apple added API to the Accelerate framework for working with sparse matrices. While Apple's APIs are designed for working with `float`

and `double`

values, with a few hacks, we can write a wrapper API that takes pointers, or more usefully, Objective-C objects.

Our `SparseMatrix`

wrapper class will have an interface that looks fairly similar to the `NSArray`

API but takes row/column pairs instead of single indices:

Since our matrices can't be resized and it's not useful having a matrix with zero elements, the parameter-less `-init`

and `+new`

initializers have been marked as unavailable. All indices and dimensions are also `NSInteger`

s so they import to Swift as `Int`

s for best impedance matching.

Now let's move to the implementation. Our underlying storage will be a `sparse_matrix_double`

:

Since `double`

s are 64-bits in size, they're sufficient to store any pointer we might pass along. Just to be safe, we can add a static assertion that verifies this:

Our initializer is fairly straightforward; we simply end up calling `sparse_matrix_create_double`

to allocate the underlying sparse matrix:

Again, we disallow creating empty matrices by throwing an exception if any of the dimensions are `0`

. Note that it's safe to cast from a `NSInteger`

to `sparse_dimension`

since a `sparse_dimension`

is a `UInt64`

, however just to be sure we're not doing a narrowing conversion, we can add a static assertion:

We might as well do this for the `sparse_index`

type we'll use later as well:

Next, let's implement our `-setObject:...`

method. This one's a one-liner:

Before adding the object, we retain it, this way our object will not go away as long as it's in the sparse matrix. We then cast its pointer to a `uint64_t`

, which is safe because pointers are at most 64-bits, and then we cast to a `double`

, which is safe because `double`

s are also 64-bits in size. Unfortunately, clang doesn't let us cast directly from a pointer to a `double`

, but that's understandable.

As you may have noticed, the call to `-retain`

implies we're not using ARC. The reason is that casting from a `uint64_t`

to a pointer is unsupported since ARC wouldn't have enough information to reason about the memory management semantics. While our `SparseObjects`

library itself doesn't use ARC, it works just fine with any clients that have ARC enabled (including Swift projects).

Now let's implement the method that will allow us to retrieve our objects:

Here, we simply delegate to `sparse_extract_sparse_row_double`

to extract values starting at the given row and column. Since we just want the value at the given row/column and no adjacent values, we pass in `1`

as the fifth argument. We also don't use the values returned by reference to `end`

and `index`

, but we can't pass `NULL`

for those parameters. Next, we cast the `double`

back to a `uint64_t`

and then back to a `pointer`

, which is the inverse of the conversion we did in the previous method. Finally, we return an autoreleased instance of the object.

Note that if the object doesn't exist, `value`

will be `0`

, which gets cast to `nil`

. Since messaging a `nil`

object in Objective-C is a no-op, this method just ends up returning `nil`

.

Our `-removeObjectAt...`

method is quite similar since we have to retrieve the object first in order to send it a `-release`

message before it gets removed:

Once the object has been released, we simply set a value of `0`

at the given row/column.

Finally, we have to consider what happens when the sparse matrix is deallocated. If there are objects stored in it, we have to release them or we'll have memory leaks.

While the code is a bit long, we simply iterate through all the rows in the matrix, and if a row has non-zero entries, we loop through all of them and send any objects a `-release`

message. Note, again, that this is a no-op if the entry is `0`

since `columnSpace[column]`

ends up getting cast to `nil`

, and messaging a `nil`

object is a no-op. Finally, we destroy the underlying matrix and our temporary buffers.

This `dealloc`

implementation isn't the fastest, however it's the price we pay for lower memory usage overall. One thing you could do is maintain around a separate set of objects that contains just the objects that are currently in the matrix. However, since the point of using sparse matrices is to reduce memory usage, I didn't do that here.

Speaking of memory usage, there is one more optimization we can make. On 32-bit systems, the implementation above will still end up storing pointers in `double`

s, however, 32-bit pointers only require half the storage space. To take this into account, we can conditionally define a set of `sparse_pointer_*`

APIs that use floating-point Accelerate APIs on 32-bit systems and double-precision Accelerate APIs on 64-bit systems:

Now everywhere you see a `double`

you'd use a `sparse_pointer_storage`

, everywhere you see a `sparse_matrix_create_double`

you'd use a `sparse_pointer_matrix_create`

, and so on.

The full implementation, complete with unit tests, can be found on GitHub.