0

I'm porting the code for an audio engine from Android/Java to iOS/Objective-c.

The code uses a List<short[]> variable to dynamically handle chunks of fixed size audio data (that needs be of the type short, i.e. exactly 2 bytes).

All chunks have the same size but there can be a dynamic number of chunks, hence the List.

For what I understand, this works well in Java because short[] is basically handled as an Object.

Now if I'm correct, I can't have a NSMutableArray of short [] in Objective-C because short [] is not an Object.

I need to keep these features (dynamic size list of fixed size chunks of shorts)

What are my options?

Stéphane
  • 1,518
  • 1
  • 18
  • 31

1 Answers1

2

You could do it the Objective-C way and have an NSArray of NSArray of NSNumbers. This is the most direct way to do it and you need not worry too much about allocating and deallocating as it all happens automatically with those Objective-C objects. There will be some conversion to get all those shorts into NSNumbers and you won't be able to access them as one single chunk or pointer, you will have to traverse them.

Here is some sample code to convert one of those and append it to your list.

@interface MyList()

@property (nonatomic,strong) NSMutableArray < NSArray * > * list;

@end

@implementation MyList

// Lazy
- ( NSMutableArray < NSArray * > * ) list
{
    if ( ! _list )
    {
        _list = NSMutableArray.array;
    }

    return _list;
}

// Work
// Return index at which it was added
- ( NSUInteger ) addShorts:( short []   ) data
          ofLength:( NSUInteger ) n
{
    NSMutableArray * m = [NSMutableArray arrayWithCapacity:n];

    while ( n )
    {
        [m addObject:@( * data )]; // Convert to short and add
        data ++;
        n --;
    }

    // Add to  list
    [self.list addObject:m];

    return self.list.count - 1;
}

// Just to test
- ( id ) init
{
    self = super.init;

    if ( self )
    {
        short data[ 4 ] = { 1, 2, 3, 4 };

        [self addShorts:data
               ofLength:4];

        NSLog ( @"How about %@", self.list );
    }

    return self;
}

@end

I think this is your best option.

However, it is certainly not your only one. If you need performance or to copy blocks of memory at a time you could use C pointers here. Since this can be blended into Objective-C easily this is a real viable option provided you know some C and are comfortable with pointers.

Those are sort of the extremes.

Then there are some inbetween options. You could e.g. wrap the short[] into NSValues and store them into the array.

Update

Based on continued discussion in comments, herewith some updated code that I used to test.

@interface MyList()

@property (nonatomic,strong) NSMutableArray < NSData * > * list;

@end

@implementation MyList

// Lazy
- ( NSMutableArray < NSData * > * ) list
{
    if ( ! _list )
    {
        _list = NSMutableArray.array;
    }

    return _list;
}

// Work
// Return index at which it was added
- ( NSUInteger ) addShorts:( short []   ) data
          ofLength:( NSUInteger ) n
{
    [self.list addObject:[NSData dataWithBytesNoCopy:data
                          length:n * sizeof ( short )
                        freeWhenDone:YES]];
    return self.list.count - 1;
}

// Print a list
- ( void ) dumpListAtIndex:( NSUInteger ) index
          ofLength:( NSUInteger ) n
{
    NSData * data = [self.list objectAtIndex:index];
    short  * p    = ( short * ) data.bytes;

    NSLog ( @"Index %lu pointer %p", index, p );
    NSLog ( @"\t%d values", ( int ) n );
    for ( int i = 0; i < n; i ++ )
    {
        NSLog ( @"\t\t%d] %d", i, ( int )( * ( p + i ) ) );
    }
}

// Just to test
- ( id ) init
{
    self = super.init;

    if ( self )
    {
        short * data = calloc( 4, sizeof( short ) );

        for ( int i = 0; i < 4; i ++ )
        {
            * ( data + i ) = ( short ) i;
        }

        [self addShorts:data
               ofLength:4];

        NSLog ( @"Allocated pointer is %p", data );
        [self dumpListAtIndex:0
                 ofLength:4];

        NSLog ( @"Change some values - is this allowed" );

        for ( int i = 0; i < 4; i ++ )
        {
            * ( data + i ) = ( short ) ( 10 - i );
        }

        [self dumpListAtIndex:0
                 ofLength:4];

    }

    return self;
}

@end

Based on this it seems you can use just NSData and not NSMutableData. It feels as if we are breaking the rules as we are changing the data pointed to by NSData.bytes but I think it is safe and even preferable, since we are not changing the pointer itself. The pointer does not and should not change, so I think NSData is better. Of course, it is crucial that you not change the length ever of the short pointer and that you not write past its end and so on.

This is a nice use case. You now have a short * wrapped inside NSData which is stored inside a NSMutableArray. So Objective-C takes care of the freeing of the pointer for you when the NSData goes out of scope, but you can still access and even change those shorts pointed to.

Performance

The pointers should do better. If the shorts are stored as NSNumber's inside an NSArray there are overhead in terms of converting to and from the NSNumber and in managing the NSArray. Here is some code you can use to test.

int main ( int argc, const char * argv [] )
{
    @autoreleasepool
    {
        // insert code here...
        NSLog(@"Hello, World!");

        // Warm up
        for ( int i = 0; i < 10000; i ++ )
        {
            for ( int j = 0; j < 10000; j ++ )
            {
                double x = atan ( i + j );
                double y = asin ( i + j );
                double z = x / ( 1 + fabs ( y ) );
            }
        }

        // Test parameters
        int n = 30;             // Arbitrary length of array of shorts
                            // Note - the way in which the test is written this should be less
                            //        than the maximum short
        int b = n * sizeof ( short );       // Size of memory we require as a result

        int l = 20000;              // Arbitrary length of list
        int m = 20;             // Arbitrary number of times we loop
        int r = 500;                // Arbitrary number of reads per loop
        int w = 20;             // Arbitrary number of writes per loop

        // Test array of arrays of shorts wrapped inside NSNumber
        NSLog(@"Start NSNumber test" );

        @autoreleasepool
        {
            NSMutableArray * list = [NSMutableArray arrayWithCapacity:l];

            // Create list
            for ( int i = 0; i < l; i ++ )
            {
                // Array of shorts
                NSMutableArray * shorts = [NSMutableArray arrayWithCapacity:n];

                for ( int j = 0; j < n; j ++ )
                {
                    [shorts addObject:[NSNumber numberWithShort:0]];
                }

                [list addObject:shorts];
            }

            // Loop
            for ( int i = 0; i < m; i ++ )
            {
                NSLog ( @"\tLoop %d/%d", i + 1, m );

                // Write tests
                for ( int t = 0; t < w; t ++ )
                {
                    for ( NSMutableArray * shorts in list )
                    {
                        // Note n must be less than maximum short
                        for ( short j = 0; j < n; j ++ )
                        {
                            [shorts replaceObjectAtIndex:j
                                      withObject:[NSNumber numberWithShort:j]];
                        }
                    }
                }

                // Read tests
                for ( int t = 0; t < r; t ++ )
                {
                    for ( NSMutableArray * shorts in list )
                    {
                        // Calculate total as read test
                        int total = 0;

                        for ( NSNumber * s in shorts )
                        {
                            total += s.shortValue;
                        }
                    }
                }
            }

            list = nil;
        }

        NSLog(@"End NSNumber test" );

        // Test array of arrays of shorts wrapped inside NSData
        NSLog(@"Start NSData test" );

        @autoreleasepool
        {
            NSMutableArray * list = [NSMutableArray arrayWithCapacity:l];

            // Create list
            for ( int i = 0; i < l; i ++ )
            {
                // NSData of shorts
                short * p = malloc ( b );

                [list addObject:[NSData dataWithBytes:p
                                   length:b]];
            }

            // Loop
            for ( int i = 0; i < m; i ++ )
            {
                NSLog ( @"\tLoop %d/%d", i + 1, m );

                // Write tests
                for ( int t = 0; t < w; t ++ )
                {
                    for ( NSData * shorts in list )
                    {
                        short * p = shorts.bytes;

                        // Note n must be less than maximum short
                        for ( short j = 0; j < n; j ++ )
                        {
                            * ( p + j ) = j;
                        }
                    }
                }

                // Read tests
                for ( int t = 0; t < r; t ++ )
                {
                    for ( NSData * shorts in list )
                    {
                        short * p = shorts.bytes;

                        // Calculate total as read test
                        int total = 0;

                        for ( int j = 0; j < n; j ++ )
                        {
                            total += * ( p + j );
                        }
                    }
                }
            }

            list = nil;
        }

        NSLog(@"End NSData test" );
    }

    return 0;
}

This code is pretty arbitrary but there are lots of parameters you can use to calibrate to your needs. On my very ancient mac mini the results look like this.

2021-01-17 11:35:56.695846+0200 ListOfShorts[733:19458] Hello, World!
2021-01-17 11:36:01.328276+0200 ListOfShorts[733:19458] Start NSNumber test
2021-01-17 11:36:01.354546+0200 ListOfShorts[733:19458]     Loop 1/20
2021-01-17 11:36:07.241021+0200 ListOfShorts[733:19458]     Loop 2/20
2021-01-17 11:36:13.136964+0200 ListOfShorts[733:19458]     Loop 3/20
2021-01-17 11:36:19.054930+0200 ListOfShorts[733:19458]     Loop 4/20
2021-01-17 11:36:24.906127+0200 ListOfShorts[733:19458]     Loop 5/20
2021-01-17 11:36:31.045219+0200 ListOfShorts[733:19458]     Loop 6/20
2021-01-17 11:36:37.189506+0200 ListOfShorts[733:19458]     Loop 7/20
2021-01-17 11:36:43.231712+0200 ListOfShorts[733:19458]     Loop 8/20
2021-01-17 11:36:49.301254+0200 ListOfShorts[733:19458]     Loop 9/20
2021-01-17 11:36:55.194978+0200 ListOfShorts[733:19458]     Loop 10/20
2021-01-17 11:37:01.021958+0200 ListOfShorts[733:19458]     Loop 11/20
2021-01-17 11:37:06.867754+0200 ListOfShorts[733:19458]     Loop 12/20
2021-01-17 11:37:12.694702+0200 ListOfShorts[733:19458]     Loop 13/20
2021-01-17 11:37:18.553492+0200 ListOfShorts[733:19458]     Loop 14/20
2021-01-17 11:37:24.389069+0200 ListOfShorts[733:19458]     Loop 15/20
2021-01-17 11:37:30.253186+0200 ListOfShorts[733:19458]     Loop 16/20
2021-01-17 11:37:36.081344+0200 ListOfShorts[733:19458]     Loop 17/20
2021-01-17 11:37:41.938501+0200 ListOfShorts[733:19458]     Loop 18/20
2021-01-17 11:37:47.760841+0200 ListOfShorts[733:19458]     Loop 19/20
2021-01-17 11:37:53.559841+0200 ListOfShorts[733:19458]     Loop 20/20
2021-01-17 11:37:59.441152+0200 ListOfShorts[733:19458] End NSNumber test
2021-01-17 11:37:59.441281+0200 ListOfShorts[733:19458] Start NSData test
2021-01-17 11:37:59.449322+0200 ListOfShorts[733:19458]     Loop 1/20
2021-01-17 11:38:00.532628+0200 ListOfShorts[733:19458]     Loop 2/20
2021-01-17 11:38:01.631107+0200 ListOfShorts[733:19458]     Loop 3/20
2021-01-17 11:38:02.736492+0200 ListOfShorts[733:19458]     Loop 4/20
2021-01-17 11:38:03.835955+0200 ListOfShorts[733:19458]     Loop 5/20
2021-01-17 11:38:04.952482+0200 ListOfShorts[733:19458]     Loop 6/20
2021-01-17 11:38:06.064449+0200 ListOfShorts[733:19458]     Loop 7/20
2021-01-17 11:38:07.167619+0200 ListOfShorts[733:19458]     Loop 8/20
2021-01-17 11:38:08.292993+0200 ListOfShorts[733:19458]     Loop 9/20
2021-01-17 11:38:09.402838+0200 ListOfShorts[733:19458]     Loop 10/20
2021-01-17 11:38:10.500115+0200 ListOfShorts[733:19458]     Loop 11/20
2021-01-17 11:38:11.596806+0200 ListOfShorts[733:19458]     Loop 12/20
2021-01-17 11:38:12.685986+0200 ListOfShorts[733:19458]     Loop 13/20
2021-01-17 11:38:13.792819+0200 ListOfShorts[733:19458]     Loop 14/20
2021-01-17 11:38:14.883854+0200 ListOfShorts[733:19458]     Loop 15/20
2021-01-17 11:38:15.985879+0200 ListOfShorts[733:19458]     Loop 16/20
2021-01-17 11:38:17.167663+0200 ListOfShorts[733:19458]     Loop 17/20
2021-01-17 11:38:18.462419+0200 ListOfShorts[733:19458]     Loop 18/20
2021-01-17 11:38:19.743327+0200 ListOfShorts[733:19458]     Loop 19/20
2021-01-17 11:38:21.044292+0200 ListOfShorts[733:19458]     Loop 20/20
2021-01-17 11:38:28.677989+0200 ListOfShorts[733:19458] End NSData test
Program ended with exit code: 0

This shows roughly two minutes for the first alternative, the array of arrays of NSNumber's and roughly half a minute when using pointers. I did not think the results would be that much different. Of course, you must attach a huge disclaimer to any performance test but yeah pointers! They are fast. PS: Earlier I said 1 minute, is 2!!!! for the first.

The write operation in the test will come at a very small penalty for the pointers. There the same region of memory is reused and just updated with new values. On the other side, with the array of arrays of shorts, the penalty is much bigger. There a new value means a new object (NSNumber) is created and this object is then inserted into the array, replacing the old value.

YMMV

skaak
  • 2,988
  • 1
  • 8
  • 16
  • Thank you. This is performance critical code both for execution time and memory, so I can't have all these conversions to NSNumber I'm afraid. Wrapping short[] into NSValue seems great though, do you have an example or some documentation on this maybe? – Stéphane Jan 14 '21 at 11:45
  • 1
    If you are really performance sensitive I'd suggest to do it *all* in C then. That said, the NSNumber conversions are highly optimised and really fast. The NSValue option is viable but again I'd go for straight C. I've done something similar at one stage using ```unsigned int``` for some low level reason as well, but there I did not use NSValue, so not a great example and also quite old. But there all the performance sensitive functionality is in C and I just wrap it in my own class to pass it around between Objective-C classes. – skaak Jan 14 '21 at 12:33
  • 1
    The moment you start using NSValue with a C pointer inside you need to start worying about allocating and free'ing that pointer. Another option is to have an array of NSData objects where you wrap the array of short into an NSData class. This can work well and you can use interesting functions such as ```[NSData dataWithBytesNoCopy:length:freeWhenDone:]``` for example. The data wraps the shorts and will take care of freeing when done (based on parameters). – skaak Jan 14 '21 at 12:42
  • 1
    Thank you, I'll go that road I think (with NSData - or NSMutableData I guess?). Should not be very different performance wise than straight C probably as the critical operations are reading/writing these shorts? – Stéphane Jan 14 '21 at 12:46
  • 1
    Yeah I think that is the way to go. If you do it right the performance should be great. You can allocate the short array and write directly to that, then wrap it into NSData and store that inside the NSMutableArray. I don't think you'd want NSMutableData but that depends of course. When you need to read you can get the short back via NSData.bytes. – skaak Jan 14 '21 at 12:49
  • Just to be sure: if I have a fixed number of shorts per chunk, I can map each chunk to a NSData object, even if change the values of the shorts over time? (as long as the number of shorts is the same and therefore the memory region stays the same) ... or do I need NSMutableData to be able to change these values? – Stéphane Jan 14 '21 at 12:53
  • I am not sure. Typically of course, when you want to change the data, you want NSMutableData. However, here the pointer stays the same and you change the values pointed to, so I think actually you could get away with NSData as the pointer does not change. What I would suggest is to use NSData and when the data changes allocate and use a new pointer, rather than reuse an old one, but this depends on the usage. I'm going to do a few tests and will get back to you on that one. – skaak Jan 14 '21 at 15:12
  • See the edits / update to my answer, where I use NSData to capture the pointer. – skaak Jan 14 '21 at 15:29
  • @skaak can you provide some benchmarking for both approaches? – Kamil.S Jan 15 '21 at 17:21
  • 1
    Thank you @skaak, much appreciated! – Stéphane Jan 15 '21 at 17:55
  • 1
    @Kamil.S Hi I added some performance. Go pointers! They did much better, better than I expected. – skaak Jan 17 '21 at 09:44