SynthMaker

asm arrays & optimisation

Archive · 2011 10 old posts

From the original SynthMaker forum (2004–2014), with its members’ names and dates.

firmament ·

any help on optimising arrays in assembler? i have a very simple code

streamin in; streamout out;

float msum; float array[850]; float write; float read;

array[write]=in; write=write+1; write=write-(write>849)&write; read=write+1;

msum=msum+in-array[read]; out=msum/850;

which calculates the moving average of an array. however, translated to assembler, it expands wildly

streamin in; streamout out;

float smIntVarTemp=0.0; int smIntVarArrayIndex=0; float smIntVarZero=0;

float msum=0; float arraymax=849; float write=0; float read=0; float F1=1; float F849=849; float F850=850; float array[850];

//ArrayIndex> sLeft=write //ArrayIndex> m_sAssignVar=array movaps xmm0,write; maxps xmm0,smIntVarZero; minps xmm0,arraymax; cvtps2dq xmm0,xmm0; movaps smIntVarArrayIndex,xmm0; //AssignmentArray> sLeft=in push eax; mov eax,smIntVarArrayIndex[0]; shl eax,4; fld in[0]; fstp array[eax]; mov eax,smIntVarArrayIndex[1]; shl eax,4; add eax,4; fld in[1]; fstp array[eax]; mov eax,smIntVarArrayIndex[2]; shl eax,4; add eax,8; fld in[2]; fstp array[eax]; mov eax,smIntVarArrayIndex[3]; shl eax,4; add eax,12; fld in[3]; fstp array[eax]; pop eax; movaps xmm0,write; addps xmm0,F1; //Assignment> sLeft=xmm0 movaps write,xmm0; movaps xmm0,write; cmpps xmm0,F849,6; movaps xmm1,xmm0; andps xmm1,write; movaps xmm2,write; subps xmm2,xmm1; //Assignment> sLeft=xmm2 movaps write,xmm2; movaps xmm0,write; addps xmm0,F1; //Assignment> sLeft=xmm0 movaps read,xmm0; movaps xmm0,msum; addps xmm0,in; //ArrayIndexFrom> sLeft=read movaps xmm1,read; maxps xmm1,smIntVarZero; minps xmm1,arraymax; cvtps2dq xmm1,xmm1; movaps smIntVarArrayIndex,xmm1; push eax; mov eax,smIntVarArrayIndex[0]; shl eax,4; fld array[eax]; fstp smIntVarTemp[0]; mov eax,smIntVarArrayIndex[1]; shl eax,4; add eax,4; fld array[eax]; fstp smIntVarTemp[1]; mov eax,smIntVarArrayIndex[2]; shl eax,4; add eax,8; fld array[eax]; fstp smIntVarTemp[2]; mov eax,smIntVarArrayIndex[3]; shl eax,4; add eax,12; fld array[eax]; fstp smIntVarTemp[3]; movaps xmm1,smIntVarTemp; pop eax; subps xmm0,xmm1; //Assignment> sLeft=xmm0 movaps msum,xmm0; movaps xmm0,msum; divps xmm0,F850; //Assignment> sLeft=xmm0 movaps out,xmm0;

basically, im unfamiliar with the way assembler handles arrays and want to know whats going on to see if theres a more efficient way to do it.

trogluddite ·

The wild expansion is due to SM's use of mainly SSE assembly, which can't directly implement arrays. SM assembly also has only a limited sub-set of the commands available in the CPU, which also makes for a few limitations.

SSE is used because it saves huge amounts of CPU load - every SSE command can process four independent streams at the same time (known as the 'channels', numbered 0 to 3). This is used very cleverly by SM's poly streams by assigning each new voice to a different sse channel, so four notes can be processed 'for the price of one'. The channels are also used more explicitly by the 'mono4' type of blue streams, where you can 'pack' and 'unpack' numbers into the four streams - e.g. to process stereo audio using only one chain of modules.

Sadly, the SSE commands don't have a direct way to access array indexes, so this has to be done using simpler instructions that can only handle one stream at a time. You will see in the assembly expansion that there is a part where the same little group of instructions are used four times over, with the four channel references in brackets (e.g. In[0], ArrayIndex[3]). You will see a similar thing for some of the maths functions too, such as sine, cosine, power etc. which also don't have direct SSE equivalents.

The maths that work out exactly what memory address to use to get at the data are done using the register 'eax'. The eax maths has to be done using integer numbers rather than floats - which also can't be done using the SSE instructions. It also means that the values have to moved around using temporary variables (you can't exchange values directly from the 'xmm' registers (SSE) to the 'eax' register.) - hence all the 'smIntVar...' variables. Because SSE has four channels, there are four values stored for each array index, and each value is a 4-byte long float number. So array indexes are spaced 16 bytes apart in memory (shl eax,4 is equivalent to *16), and each SSE channel is spaced 4 bytes apart (so, add eax,4 for channel [1], add eax,8 for channel [2] etc.)

Given the limitations above, the assembly is actually quite close to being optimal - but there are a couple of things that you can do... 1) The 'minps' and 'maxps' lines can often be deleted. They are there only to ensure that the array index cannot be out of range (which would cause a crash!)- but if you are certain that your value will always be valid, you can do without them. 2) If you intend only to use your code for mono applications (never poly or mono4), then only one SSE channel ever needs to be processed. In this case you can delete the duplicate sections for channels [1] to [3], and just keep the channel [0] part (and the initial bit that generates the Index pointer).

You can also find some further optimisations in the thread HERE - but some of those will only work in special circumstances.

You will probably find the list HERE useful - it shows all of the opcodes that the assembler recognises, each with a short description, and highlghts which ones are SSE

MyCo ·

@firmament: Check the values of your read variable, it can go up to 850, but the highest allowed index is only 849. In the ASM version you can find some minps and maxps, that keep S|M from crashing, because you try to access memory that doesn't belong to the array that you are reading from.

firmament ·

MyCo wrote: @firmament: Check the values of your read variable, it can go up to 850, but the highest allowed index is only 849. In the ASM version you can find some minps and maxps, that keep S|M from crashing, because you try to access memory that doesn't belong to the array that you are reading from.

you, sir, are very clever. i've corrected this and also rearranged the code - due to the sequencing the read pointer was 2 ahead of the write.

trogluddite wrote: The wild expansion is due to SM's use of mainly SSE assembly, which can't directly implement arrays. SM assembly also has only a limited sub-set of the commands available in the CPU, which also makes for a few limitations.

SSE is used because it saves huge amounts of CPU load - every SSE command can process four independent streams at the same time (known as the 'channels', numbered 0 to 3). This is used very cleverly by SM's poly streams by assigning each new voice to a different sse channel, so four notes can be processed 'for the price of one'. The channels are also used more explicitly by the 'mono4' type of blue streams, where you can 'pack' and 'unpack' numbers into the four streams - e.g. to process stereo audio using only one chain of modules.

Sadly, the SSE commands don't have a direct way to access array indexes, so this has to be done using simpler instructions that can only handle one stream at a time. You will see in the assembly expansion that there is a part where the same little group of instructions are used four times over, with the four channel references in brackets (e.g. In[0], ArrayIndex[3]). You will see a similar thing for some of the maths functions too, such as sine, cosine, power etc. which also don't have direct SSE equivalents.

The maths that work out exactly what memory address to use to get at the data are done using the register 'eax'. The eax maths has to be done using integer numbers rather than floats - which also can't be done using the SSE instructions. It also means that the values have to moved around using temporary variables (you can't exchange values directly from the 'xmm' registers (SSE) to the 'eax' register.) - hence all the 'smIntVar...' variables. Because SSE has four channels, there are four values stored for each array index, and each value is a 4-byte long float number. So array indexes are spaced 16 bytes apart in memory (shl eax,4 is equivalent to *16), and each SSE channel is spaced 4 bytes apart (so, add eax,4 for channel [1], add eax,8 for channel [2] etc.)

thank you so much! its all a lot clearer now 🙂 this is exactly what i was hoping someone could explain

i am already using the pack4 module (and in fact one of your custom modules to get a mono stream out of the packed data). its very handy!

trogluddite wrote: Given the limitations above, the assembly is actually quite close to being optimal - but there are a couple of things that you can do... 1) The 'minps' and 'maxps' lines can often be deleted. They are there only to ensure that the array index cannot be out of range (which would cause a crash!)- but if you are certain that your value will always be valid, you can do without them. 2) If you intend only to use your code for mono applications (never poly or mono4), then only one SSE channel ever needs to be processed. In this case you can delete the duplicate sections for channels [1] to [3], and just keep the channel [0] part (and the initial bit that generates the Index pointer).

i have removed the min & max lines, shaving off about 8 cycles. I have two further ideas:

1) could i just declare the pointer as an int an save the conversions as well?

2) if i am using pack4 but only packing into first 2 channels can i ignore the other two? or does SSE make use of all 4 channels even if only 2 have data input to them?

trogluddite wrote: You will probably find the list HERE useful - it shows all of the opcodes that the assembler recognises, each with a short description, and highlghts which ones are SSE

i have discovered this already, i keep it handy whenever i open sm 😉

trogluddite ·

Glad you found that useful 🙂

Your new optimisation ideas are perfectly sound...

1) You can declare an integer variable, however the difficulty is that none of the SSE maths or comparison operations will work correctly on integers (the 'ps' in the instruction names means 'packed single-precision floats'). Actually, there are CPU instructions to do it for integers, but they are ones that SM does not support (has been requested many times, so that we can tidy up our array access a bit!) For your particular case (all SSE channels counting the same loop length in sync with each other), it would be possible to calculate the pointer as an integer directly using 'eax' - incrementing it by 16 each time to move onto the next array entry, and storing the result in an integer variable. This is much simpler if you change your buffer length to be a power of two so that a bitmask could be used to mask the 'eax' value to make a circular buffer - similar to the examples on the 'quick sine' thread.

2) Yes, you are spot on - the 'spare' two channels can just be left out (they would only end up filled with zero's anyway).

trogluddite ·

I couldn't resist the temptation to see just how far the array access could be 'hot-rodded' - so I hope I'm not spoiling your fun by posting this!...

The optimisations only work because this is a very special case - the same array index will always be used for all four SSE channels AND the buffer size is carefully chosen to be a power of 2 (1024 in this case). Apart from storing the value in the 'Pointer' variable between samples, all the pointer calculation is done in integers directly on eax - stepping through the channels and indexes just by adding offsets to the current value.

In the '2 channel' version, I've done something really "dangerous"!! Because we're only interested in 2 channels, only half the amount of memory is really needed - only 8 bytes per index. So I fooled SM into doing that by halving the buffer size, and stepping 8 bytes per index.

I've been messing a lot with tricking SM into organising the array data differently for a while, as I use a LOT of looping type effects - but I'd understand if folks think I'm must be a couple of bits short of a byte taking things so far! Is there reason to my madness? (or do I just enjoy the crashes when I get the numbers wrong?)...

Smaller buffers, and less jumping around between indexes, means that it is more likely that the required data is already in the CPU cache (super fast access memory). When the memory addresses being accessed are further apart, it's more likely that the main memory on the motherboard will have to be accessed - the dreaded 'cache misses' A single cache miss can cost several tens of CPU cycles! - so re-organising the array(s) to make the data more 'compact' can potentially save far more CPU cycles than even the most skillful re-arranging of instructions. If you only have a few arrays, it's not such a problem, but if you use a lot of them or they are very big, there's potentially a lot of CPU cycles to be saved.

Apologies again for hi-jacking the thread - it has a good name for when folks are searching for this kind of stuff (thankyou, firmament, it's amazing how often people forget to use a helpful thread name!) - could be a good place to pool together any other array type tweaks that anyone has to add.

firmament ·

sorry for the horribly late reply, ive been swimming in exams and had no time to sit down and type out a full response.

trogluddite wrote: I couldn't resist the temptation to see just how far the array access could be 'hot-rodded' - so I hope I'm not spoiling your fun by posting this!...

Not at all, the thing im enjoying most about synthmaker is learning and this is brilliant teaching material!

trogluddite wrote: In the '2 channel' version, I've done something really "dangerous"!! Because we're only interested in 2 channels, only half the amount of memory is really needed - only 8 bytes per index. So I fooled SM into doing that by halving the buffer size, and stepping 8 bytes per index.

Wow! this is a LOT faster. around a third the speed of my original one. had no idea it could be optimised this far!

trogluddite wrote: I've been messing a lot with tricking SM into organising the array data differently for a while, as I use a LOT of looping type effects - but I'd understand if folks think I'm must be a couple of bits short of a byte taking things so far! Is there reason to my madness? (or do I just enjoy the crashes when I get the numbers wrong?)...

i think anyone even an iota away from sanity would seriously struggle to write that kind of code 😉

there is something satisfying about seeing that crash message appear though. reminds you that really computers are just glorified pocket calculators

trogluddite wrote: Smaller buffers, and less jumping around between indexes, means that it is more likely that the required data is already in the CPU cache (super fast access memory). When the memory addresses being accessed are further apart, it's more likely that the main memory on the motherboard will have to be accessed - the dreaded 'cache misses' A single cache miss can cost several tens of CPU cycles! - so re-organising the array(s) to make the data more 'compact' can potentially save far more CPU cycles than even the most skillful re-arranging of instructions. If you only have a few arrays, it's not such a problem, but if you use a lot of them or they are very big, there's potentially a lot of CPU cycles to be saved.

makes sense. where on earth did you find all this stuff out? the manual doesnt quite seem to cover it. haha

as i have contributed very little to this thread so far i shall offer up a few (unrelated) optimised assembler modules... EDIT : moved here

infuzion ·

firmament wrote: trogluddite wrote: In the '2 channel' version, I've done something really "dangerous"!! Because we're only interested in 2 channels, only half the amount of memory is really needed - only 8 bytes per index. So I fooled SM into doing that by halving the buffer size, and stepping 8 bytes per index.

Wow! this is a LOT faster. around a third the speed of my original one. had no idea it could be optimised this far!

Troggie is great at optimizing, esp arrays, since he thinks more outside the box!

firmament wrote: Makes sense. where on earth did you find all this stuff out? the manual doesnt quite seem to cover it. haha

[old link] Most (older) entry-level computer text books have some of this info, though newer CPUs require reading the manufacturers' tech PDFs. I've even learned CPU stuff from Tom's Hardware.

firmament wrote: as i have contributed very little to this thread so far i shall offer up a few (unrelated) optimised assembler modules. 3 fairly minimal versions of basic synth-building blocks: first is an oscillator (sin tri & saw). no antialiasing/bandlimiting on the sharp waves whatsoever so of limited usefulness on its own.....

Good stuff, but is likely better as its own thread in "examples" 😉

firmament ·

infuzion wrote: firmament wrote: trogluddite wrote: In the '2 channel' version, I've done something really "dangerous"!! Because we're only interested in 2 channels, only half the amount of memory is really needed - only 8 bytes per index. So I fooled SM into doing that by halving the buffer size, and stepping 8 bytes per index.

Wow! this is a LOT faster. around a third the speed of my original one. had no idea it could be optimised this far!

Troggie is great at optimizing, esp arrays, since he thinks more outside the box!

firmament wrote: Makes sense. where on earth did you find all this stuff out? the manual doesnt quite seem to cover it. haha

[old link] Most (older) entry-level computer text books have some of this info, though newer CPUs require reading the manufacturers' tech PDFs. I've even learned CPU stuff from Tom's Hardware.

thanks, ive skim read and bookmarked a couple of these for later. should be useful!

infuzion wrote: firmament wrote: as i have contributed very little to this thread so far i shall offer up a few (unrelated) optimised assembler modules. 3 fairly minimal versions of basic synth-building blocks: first is an oscillator (sin tri & saw). no antialiasing/bandlimiting on the sharp waves whatsoever so of limited usefulness on its own.....

Good stuff, but is likely better as its own thread in "examples" 😉

i shall move them to their own thread right away then 😉

trogluddite ·

Glad to hear that you've found it useful. 🙂

firmament wrote: where on earth did you find all this stuff out?

In the case of array optimisation, maybe it's as simple as having worked as a warehouse storekeeper! A truly lazy (but clever) storeman knows that it's not just about keeping everything in its place - you can save yourself an awful lot of work (and have more time for coffee breaks!) if you put the most popular items closest to the delivery bay!

infuzion wrote: thinks more outside the box!

Or maybe thinking inside the box... Once you're inside a code/asm box, the only thing that matters is that the right numbers get read from somewhere, processed and then sent somewhere else. The only limitation on how you achieve that is the instruction set. I try to think of code/asm as not part of SM, but as a 'plugin within a plugin', with the streamin's and streamout's as the 'interface' with SM - as long as the flow of number in and out is right, there is no specific SM way that things have to be arranged. This is why I now prefer to 'translate' my ASM directly from my core algorithm (often just a flowchart) - taking the SM generated version of ASM code as a starting point for optimising can sometimes make it much harder to see alternative solutions.

Also helps that I started programming back in the 80's - "necessity is the mother of invention", as they say; and in those days, hand optimised assembly was the only way to get decent performance out of the slow 8bit CPU's. I still think that assembly is one of the best ways to learn programming - having to break things down into such small steps really fine tunes your awareness of how to get the best out of an algorthm, and teaches you things that are valuable no matter what your usual programming language might be.

Wrote something here? To have a post removed or your name hidden, write to [email protected].

Before your first post

A few rules keep this a friendly place. You agree to them once.

Shown next to what you post, here and in the app. It doesn’t have to be your real name: 3–24 lower-case letters, digits or _ (capitals become lower case, spaces become _).

    Then your post goes up straight away.

    Delete this reply?

    Open it in SynthMaker?

    It goes on the track you’re editing there, in place of that track’s sound, and into My sounds. Your other tracks stay as they are.

    Want to keep that track’s sound? Start a new song in SynthMaker first (Song › New song), then open this.

    Report this reply?

    Only the developer reads reports. The person isn’t told who reported them.

    What’s wrong with it?

    Block this person?

    You won’t see their posts and replies here any more. They aren’t told, and you can unblock them at any time.

    People you’ve blocked

    Their posts and replies are hidden from you. Unblock someone to see them again.

    The community rules

    A few rules keep this a friendly place. Everyone agrees to them once, before their first post.