From the original SynthMaker forum (2004–2014), with its members’ names and dates.
infuzion ·
trogluddite wrote: I usually reserve a block of syntax definitions in notepad++ for variable names, to make sure my spelling is consistent - hence I though my code was thoroughly checked. But of course,a duplicate is still spelled right, so my check wasn't as thorough as I thought!
My usual practice is to use CamelCase variable/array names that are descriptive, it make debugging so much easier - e.g. LeftBuffer[DelayCount], AllDoneFlag, etc...
Using a capital first also helps separate variables visually from SM functions & ASM commands.
Using a real text editor allows you to save .ASM & Code separately, global search & replace, user editable code highlighting, macros, & much more!
trogluddite ·
Second that! Once you have tried it, you will wonder how you ever managed without it.
I've used Notepad++ for a long time now, very friendly and configurable (and free!) - my favourite feature is being able to have the same code open in two windows that are both editable, so that I don't have to keep scrolling up to add new variable declarations!
There's a Notepad++ syntax definition file for SM's assembly primitive in this old post so that you can get up and running quckly.
When I'm back on my main machine, I'll have a poke around, I think I've updated it a bit since then.
infuzion ·
trogluddite wrote: There's a Notepad++ syntax definition file for SM's assembly primitive in this old post so that you can get up and running quckly.
Here is mine; now finish that trigger tutorial please! /cracks whip 😉
trogluddite ·
infuzion wrote: Here is mine; now finish that trigger tutorial please! /cracks whip
Yes, I'll try to do the next lesson this weekend, boss! I couldn't quite face SM while my latest baby was poorly with array bugs and random crashes, but Malc made it all better - so I shall repay his efforts by helping out with the missing SM documentation! 😉
mwvdlee ·
Seems I found a solution for my long-standing bug. It's not a generic solution - the envelope control is still buggy - but it works specifically for my situation. It's possible it may apply to some other people's code, so I'll try and work out a more generic demonstration.
In the mean time I was trying to optimize some ASM code and found the following:
Code...
Perhaps maxps is incredibly slow or something?
Am I missing something or would this be a generic improvement worthy of replacing the current abs() implementation?
One instruction more, but both rcpps and mulps are cheap.
This little trick applied to just ONE case decreased CPU from ~5.2% to ~5.0%; 4% improvement!
FYI, In this context, the reciprocal of a number is the same as dividing 1 by that number; reciprocal of A is 1/A.
If you do more than one division by the same number, you could even take the time to calculate the reciprocal of the divisor in high precission and multiply by that number:
Even though the latter uses more code, the advantage of having only one divps and two mulps versus two divps is quite high.
Ofcoure, if the divisor is static or coming from outside the code module, you should precalculate the reciprocal 🙂
Doing that decreased my ~5.2% 0.1% further to ~4.9% (whilst improving quality if rounded to sufficient digits). These sound like minor improvements, but if you add up many of these simple changes, the performance difference can be dramatic.
Mo ·
mwvdlee wrote: Ofcoure, if the divisor is static or coming from outside the code module, you should precalculate the reciprocal
Also it can be calculated in stage(0).
mwvdlee ·
Mo wrote:
mwvdlee wrote: Ofcoure, if the divisor is static or coming from outside the code module, you should precalculate the reciprocal
Also it can be calculated in stage(0).
Indeed. Atleast that would guarentee good quality for those numbers that don't round out well.
Otherwise; why waste the cycles? 😉 AFAIK stage(0) is called for every triggered poly (should also be for every re-triggered poly, but it doesn't).
I've got some other trick I'd like to share here, most probably well known already but it doesn't hurt to document them for those who don't.
stw ·
Hi mwvdlee,
good to hear you could succeed with that annoying bug!
About the abs routines:
Though your math is a clever solution i guess it won't result into much of a difference to the SM code. I think behind the scene maxps is doing exactly what the code describes; a cmp/and/add.
Most saving abs trick is the so called "abs mask" discovered by sambean IIRC decaes ago!
streamin in;
streamout out;
int absmask=2147483647;
There some threads relating to asm optimizing e.g:
Code: sqrt(), abs(), pow(e,b), denorm(x,lim), +much more...
Code tips x a beginner
Regarding the fastest code i recently found that list which covers a variety of comparisons for (many) different chips.
http://www.agner.org/optimize/instruction_tables.pdf
maybe it's useful for you or anyone else
oh...and new tips&tricks are always appreciated! 😀
trogluddite ·
mwvdlee wrote: dif = abs(in);
There is an even cheaper, sneaky, way to do this...
int absmask=2147483647; //must be exactly this number!!
The integer 'absmask' creates a btmask which allows all of a float number through the bitwise andps - except for the sign bit, which gets set to zero (the number is the largest possible positive 32bit integer).
mwvdlee wrote: you could even take the time to calculate the reciprocal
Another closely related tip...
Try and avoid this kind of dependency...
In this case, the add and multiply have to wait until the division is finished before they can begin their calculations. But with a modern CPU, there are multiple execution units that can get on with executing some other instructions whlile the divps does its stuff - as long as those instructions are not waiting for the division result.
So if you really need a divide, try and set it running 'early', and put some instructions from an unrelated part of the code immediately after it - so that something else useful is being done while the divide calculates.
These kind of 'dependency chains' occur with all instructions, but are particularly bad when following a divide. I was quite stunned when I went through some of my code (folowing infuzion's advice), and 'interleaved' some of the lengthy equations - letting the CPU execute some instructions out-of-order to allow some breathing space for the greedy ones. I've seen savings of nearly 20% on some algorithms!
(But if you do this, document it well - it does make assembly a lot harder to read when your algorithm is 'leap-frogging' over itself a lot!
mwvdlee ·
Come to think, shouldn't these tips be in the wiki somewhere? Just a generic ASM optimizing topic ranging from simple to more specialized tips? Since ASM performance is relatively important to SM, it should be well documented.
And Malc should just implement more SSE instructions so we don't need to use workarounds for something already natively supported by the CPU. I find it incredibly (and inexcuseable) that something as obvious as XORPS is missing, let alone some of the more specialized stuff. I guess he's just afraid op ASM support becoming powerful enough for us not to have to rely on Outsim for new stuff. Which would be fine if they actually made new stuff.
infuzion ·
Lots of great tips!
trogluddite wrote: Try and avoid this kind of dependency...
divps xmm0,number;
addps xmm0,AnotherNumber;
mulps xmm0,Avariable;
In this case, the add and multiply have to wait until the division is finished before they can begin their calculations. But with a modern CPU, there are multiple execution units that can get on with executing some other instructions while the divps does its stuff - as long as those instructions are not waiting for the division result. So if you really need a divide, try and set it running 'early', and put some instructions from an unrelated part of the code immediately after it - so that something else useful is being done while the divide calculates.
(But if you do this, document it well - it does make assembly a lot harder to read when your algorithm is 'leap-frogging' over itself a lot!
Good tip, but ensure if you move opcodes around that the results are stored in an XMMx that is not being used around the new location.
mwvdlee wrote: Division
divps xmm0,F4410;
If you don't need the precission of divps, but could settle for 12 bit precission, you could multiply the reciprocal instead.
rcpps xmm0,xmm0;
mulps xmm0,total;
One instruction more, but both rcpps and mulps are cheap.
Not that much cheaper;
Core2Duo 65mm latency: divps 6-18, rcpps + mulps 7
AMD k10: divps 18, rcpps + mulps 7
Newest CPUs are likely to have faster divps. You do save CPU with rcpps, but the gap is smaller than what people assume sometimes. If you feel that you need the extra bits of precision (eg filters, mastering), the use the full divps.
mwvdlee wrote: Come to think, shouldn't these tips be in the wiki somewhere? Just a generic ASM optimizing topic ranging from simple to more specialized tips? Since ASM performance is relatively important to SM, it should be well documented.
Anyone with a forum logon can edit the WiKi, so you nominate yourself?
mwvdlee wrote: And Malc should just implement more SSE instructions so we don't need to use workarounds for something already natively supported by the CPU. I find it incredibly (and inexcuseable) that something as obvious as XORPS is missing, let alone some of the more specialized stuff.
I want to email Malc with a clear & prioritized plan. You can help by posting on the new 64bit audio request thread here.
mwvdlee ·
infuzion wrote: Not that much cheaper;
Core2Duo 65mm latency: divps 6-18, rcpps + mulps 7
AMD k10: divps 18, rcpps + mulps 7
Newest CPUs are likely to have faster divps. You do save CPU with rcpps, but the gap is smaller than what people assume sometimes. If you feel that you need the extra bits of precision (eg filters, mastering), the use the full divps.
This (http://www.intel.com/content/dam/doc/ma ... manual.pdf) recent (july 2011) document by Intel claims the rccps method is about 6x faster. It claims DIVPS is 14 cycles and, more importantly, non-pipelined. (on that topic; I know MULPS and ADDPS can be pipelined and DIVPS cannot, but what about the other ops? SUBPS? ANDPS?).
I should note that precission is much higher if you can reasonably precalculate the reciprocal yourself: mulps xmm0,FHALF is about as accurate as divps xmm0,F2, but significantly faster.
The Intel document also states a "newton-raphson" approximation, which is 22 bit precise:
Apparently "only" 2.7x faster than plain DIVPS, but almost as precise (and likely more that you need for 16-bit output).
Sadly, most other tricks in that Intel document don't work, simply because SM lacks some of the most basic operands such as JMP, SQRTPS, XORPS, CMP*PS.
I also tried to interleave SSE ops with scalar (FP, X87, "old") ops and got a slight improvement of about 0.1%. Not much, but if you use lots of scalar code, it may be worthwhile, and it's very easy to do.
trogluddite ·
Something else which has saved a considerable number of cycles for me recently is optimising arrays and buffers.
Deleting the scalar op's that relate to SSE channels that you aren't using is pretty commonplace - but for arrays and buffers, it's worth thinking about the redundant memory too (since arrays always reserve space for all 4 channels even if you don't use them).
For example if you are using mono4 to save CPU when processing stereo signals, consider 'streamlining' the way that arrays are accessed so that you don't have 2 channels worth of redundant empty array space (e.g. shl eax,3; instead of the usual shl eax,4;, and halve the array size )
As well as the 50% saving on memory, and fewer scalar op's to fill the 'silent' SSE channels, this can seriously cut down on cache misses. Effectively, twice as much useful data is now present on one cache line, so it will be longer before this data is all read out, resulting in less calls to the main memory (loading a new cache line can cost dozens of clock cycles)
And, of course, for modules intended for mono streams, you cut everything down to 1/4 of the original size.
Similarly if you have a block of ASM or code that has multiple arrays, it can be worth trying to put the data into one, larger, array. i.e. keep data that will be accessed close together (in time) as near neighbours (in memory). Combining multiple code primitives into one larger block can also help, for the same reason of 'closeness' of the data in memory for any given sample.
When using the CPU analyser on delays and buffers, you can often see the effect quite clearly - the CPU graph will mostly show a pretty steady reading - but with regular(ish) spikes, lasting only one audio sample, on iterations where the code stalls as it waits for a new cache line to be loaded.
Andrew J ·
mwvdlee wrote: Sadly, most other tricks in that Intel document don't work, simply because SM lacks some of the most basic operands such as JMP, SQRTPS, XORPS, CMP*PS.
SM does have sqrtps!
infuzion ·
Andrew J wrote:
mwvdlee wrote: Sadly, most other tricks in that Intel document don't work, simply because SM lacks some of the most basic operands such as JMP, SQRTPS, XORPS, CMP*PS.
SM does have sqrtps!
I don't know why SM would need JMP, but other J* jump opcodes are there; just do an always-true compare. CMP & CMPPS are also there. mwvdlee, I think you are making statements of fact before testing them; can you please post an OSM that proves that you have saved that many cycles with RCCPS please?
That "newton-raphson" approx is interesting; thanks for that!
BTW, here is XOR without XOR, thanks to George Boole:
mwvdlee ·
infuzion wrote: I don't know why SM would need JMP
Because that's how you do if...else in ASM if you are forced to work inline.
infuzion wrote: but other J* jump opcodes are there; just do an always-true compare
I use ASM to improve performance. Adding instructions in order to emulate missing instructions is counter productive.
You do have a point though... other J* opcodes are there... so why not the most obvious; JMP?
infuzion wrote: ...slow and error-prone xor implementation...
ORPS, ANDPS and SUBPS where a single XORPS would have sufficed. It's a waste of CPU cycles and all because a very basic bitwise operand is missing.
Dozius ·
trogluddite wrote: For example if you are using mono4 to save CPU when processing stereo signals, consider 'streamlining' the way that arrays are accessed so that you don't have 2 channels worth of redundant empty array space (e.g. shl eax,3; instead of the usual shl eax,4;, and halve the array size )
As well as the 50% saving on memory, and fewer scalar op's to fill the 'silent' SSE channels, this can seriously cut down on cache misses. Effectively, twice as much useful data is now present on one cache line, so it will be longer before this data is all read out, resulting in less calls to the main memory (loading a new cache line can cost dozens of clock cycles)
And, of course, for modules intended for mono streams, you cut everything down to 1/4 of the original size.
I can't believe I never thought of this. It's so obvious once it's staring you in the face. I have some code this would work wonders on. Thanks!
trogluddite ·
Dozius wrote: I can't believe I never thought of this. It's so obvious once it's staring you in the face.
Know just what you mean - found this one out the hard way when trying to optimise some delays. I knew that my code ought to have been more efficient, as the opcode/latencies count was way lower. However, I had changed the buffer sizes to be powers of two (so that a bitmask could be used to keep the index in range more economically).
After much head-scratching, I found that the increase in buffer size was leading to far more cache misses than the original code I was using for comparison. As soon as I changed the old code to have the new larger buffer size (to compare like with like), the CPU savings from my optimisations suddenly revealed themselves.
Then I went to do some research on the net, and found that this kind of 'data arrangement' stuff is standard practice for most game/database programmers - I felt such a fool that I'd never followed up any of the hundreds of links to the subject before!
Some more on this subject in the Fast Sine Cosine thread.
MDMA ·
Hi guys,
In ASM, what does the following do?
cmpps xmm0,F0P5,6;
It compares xmm0 with F0P5, that I understand,but what does the "6" after that means?
And does xmm0 holds a new value from here on?
Thanks in advance!
Mo ·
MDMA wrote: It compares xmm0 with F0P5, that I understand,but what does the "6" after that means?
"6" means ">". There's also other compare options:
0 ==
1 <
2 <=
3 unord
4 <>
5 >=
6 >
7 ord
MDMA wrote: And does xmm0 holds a new value from here on?
Yes. It will be a Boolean. Use "and" with 1 to make it 0 or 1.
MDMA ·
Mo wrote:
MDMA wrote: It compares xmm0 with F0P5, that I understand,but what does the "6" after that means?
"6" means ">". There's also other compare options:
0 ==
1 <
2 <=
3 unord
4 <>
5 >=
6 >
7 ord
MDMA wrote: And does xmm0 holds a new value from here on?
Yes. It will be a Boolean. Use "and" with 1 to make it 0 or 1.
Thanks a lot mate, you've unravelled the mistery of the last number for me! 😉
trogluddite ·
P.S. By total coincidence, I just posted an explanation of the mysterious "ord" and "unord" options - you can read it HERE.
MDMA ·
trogluddite wrote: P.S. By total coincidence, I just posted an explanation of the mysterious "ord" and "unord" options - you can read it HERE.
Thanks m8!
infuzion ·
MDMA wrote: Hi guys, in ASM, what does the following do?
cmpps xmm0,F0P5,6;
It compares xmm0 with F0P5, that I understand,but what does the "6" after that means?
And does xmm0 holds a new value from here on?
Mo gave you the correct advice, & let me expand on it: I use either docs from Intel or AMD to figure out OpCodes, though the last few years I find a simple websearch works if you know to put "ASM" &/or "opcode" with the search:
https://duckduckgo.com/?q=asm+cmpps
The first operand (xmm_, or a variable/memory address) is always the "destination" for results.
MDMA ·
I'm having a hard time understanding how float variables work here...
in the following code:
I'm moving status into xmm4, making xmm4 equal to "0" right?
Then why cant I use xmm4 instead of env0 at the final bit of the code?
Wouldn't that make xmm6 equal to "0" as well?
Mo ·
MDMA wrote: I'm moving status into xmm4, making xmm4 equal to "0" right?
Then why cant I use xmm4 instead of env0 at the final bit of the code?
Wouldn't that make xmm6 equal to "0" as well?
Yes, it will be the same.
..or did i miss something?
MDMA ·
Mo wrote: ..did i miss something?
Thats what I've been thinking!!!
Here's the full code:
When I (movaps xmm6,xmm4), it breaks the code and the envelope stops working???
trogluddite ·
Ah yes, it definitely needed the full code to make sense of this.
The values "env0" and "status" both get changed and stored again later on in the code.
The initial "float xxx=nnn" lines only happen when the code is first compiled - after that, their values get stored from one sample to the next using only the values calculated in the main code.
However, this does not apply to the xmm registers - at the first line of a code/asm, you can never know what value the xmm registers will have - they are very temporary storage built into the CPU core, and are used all over the place by other processes - so you cannot treat them quite the same as normal variables.
You are changing...
movaps xmm6,env0;
...to...
movaps xmm6,xmm4;
But doing that is equivalent to writing...
movaps xmm4,status;
...as the "status" stuff in the previous few lines is the last thing that smm4 was used for. At this point in the code the value of "env0" will be whatever value it was left with when the code was used on the previous sample. So "env0" and xmm4 are definitely not interchangeable.
MDMA ·
trogluddite wrote: You are changing...
movaps xmm6,env0;
...to...
movaps xmm6,xmm4;
But doing that is equivalent to writing...
movaps xmm4,status;
...as the "status" stuff in the previous few lines is the last thing that smm4 was used for. At this point in the code the value of "env0" will be whatever value it was left with when the code was used on the previous sample. So "env0" and xmm4 are definitely not interchangeable.
1st, thanks for the help!!!
Now let me see if I got you right:
You are saying that because this happened:
movaps status,xmm0;
xmm4 holds now a new value?
If so can it be any value it picked up from other processes in the computer or is it equal to xmm0 now?
trogluddite ·
xmm4 gets the status value a few lines earlier...
movaps xmm4,status; //move status into xmm4
andps xmm0,xmm4;
movaps status,xmm0;
movaps xmm0,loLimit;
movaps xmm6,env0; //xmm4 still equals status
...and remember, when status is copied into xmm4, status has whatever value it ended up with from the previous sample - and those lines of code later on may well have changed it.
The andps and other lines don't change the value of xmm4, so it is still there when you get to the line you were editing.
This follows the general rule for assembly - when you have two (or more) arguments, the first one is always the destination, and the others don't get changed.
A few examples...
movaps xmm4, status; //status copied into xmm4, status does not change
andps xmm0,xmm4; //xmm0 get the value of the "and", xmm4 not changed
movaps status,xmm0; //xmm0 copied into status, xmm0 not changed
etc...
MDMA ·
trogluddite wrote: ...and remember, when status is copied into xmm4, status has whatever value it ended up with from the previous sample
But status is suposed to be 0 when it gets assigned to xmm4. no?
Mo ·
MDMA wrote: But status is suposed to be 0 when it gets assigned to xmm4. no?
Yes but only the very first sample, then it will be overwritten.
trogluddite ·
MDMA wrote: But status is suposed to be 0 when it gets assigned to xmm4. no?
For the first sample, certainly - but after that, it all depends on the rest of the code...
The line "float status=0" isn't really part of the code at all - it is just used by SM to reserve a space in memory for the variable, and it puts in a start value. When the code is running with real audio, the "float" lines ("declarations") are ignored.
So, status does equal zero for the very first sample. However, if you look near the end of the code there is another line "movaps status,xmm0" which writes a new value.
When the next sample comes around, status will still have this new value because variables (not xmm's) get remembered between samples.
I would guess from the code that it is an envelope of sorts, so status is probably being used as an indicator to say when the envelope is running - it is probably only zero when the envelope is not doing anything, and changes to some other value when the attack starts.
MDMA ·
trogluddite wrote: When the next sample comes around, status will still have this new value because variables (not xmm's) get remembered between samples.
So registers don't get remembered?
I don't understand, do registers run once per sample while float variables stay in the background or vice versa?
Sorry for being such a pain...
stw ·
MDMA wrote:
trogluddite wrote: When the next sample comes around, status will still have this new value because variables (not xmm's) get remembered between samples.
So registers don't get remembered?
I don't understand, do registers run once per sample while float variables stay in the background or vice versa?
Sorry for being such a pain...
Yes you understand right:
Registers maintain its values for one cycle in the asm block. Variables hold it until changed.
CPU Registers are accessed used from every part of the program and change their values all over the place according the ordered processing of an application.
Variables are fixed locations in the RAM which are presereved and can only be accessed if explicitely adressed.
martinvicanek ·
stw wrote: CPU Registers are accessed used from every part of the program and change their values all over the place according the ordered processing of an application. Variables are fixed locations in the RAM which are presereved and can only be accessed if explicitely adressed.
Thanks for this insight, stw! That explains how DWB's xmm register watcher works. 🙂
trogluddite ·
martinvicanek wrote: Thanks for this insight, stw! That explains how DWB's xmm register watcher works.
And also its limitation - it can only "see" a snapshot of the register value at a particular point in the execution order. So needs care to use if there are multiple primitives/modules as it's not always clear where the value comes from.
MDMA wrote: Sorry for being such a pain...
Not a problem at all - you do the right thing by asking. Even people with lots of code experience often get confused by assembly at first - other programming languages rarely let you access the registers directly.
And never forget - the most important code in assembly is this one...
//Helpful comment so I can remember what the hell this code is doing
Once you find your feet and start optimising, you will find that variables get used less and less, and the "xmm's" get used more and more - and "xmm" isn't very helpful compared to, e.g. "VariableForThatReallyImportantThing".
Those double-slashes "//" are your bestest friend - without them it is sometimes hard to understand assembly that you wrote only a few hours ago!
Another little tip - it's quite rare to write directly in assembly; mostly people will check out their idea using code first. When I do this, I keep the original code in a small purgable module next to the assembly, so that it is always there to refer back to. As long as it isn't connected to anything, it won't use any CPU, and setting it to "purgable" means it will get deleted automatically when you export to VST etc. This has saved me many times when trying to de-bug a complex assembly routine - the old code tells me just what the assembly is supposed to be doing.
MDMA ·
Thanks a lot guys, it now makes more scence! 😉
trogluddite wrote: And never forget - the most important code in assembly is this one...
//Helpful comment so I can remember what the hell this code is doing
Once you find your feet and start optimising, you will find that variables get used less and less, and the "xmm's" get used more and more - and "xmm" isn't very helpful compared to, e.g. "VariableForThatReallyImportantThing".
Those double-slashes "//" are your bestest friend - without them it is sometimes hard to understand assembly that you wrote only a few hours ago!
Another little tip - it's quite rare to write directly in assembly; mostly people will check out their idea using code first. When I do this, I keep the original code in a small purgable module next to the assembly, so that it is always there to refer back to. As long as it isn't connected to anything, it won't use any CPU, and setting it to "purgable" means it will get deleted automatically when you export to VST etc. This has saved me many times when trying to de-bug a complex assembly routine - the old code tells me just what the assembly is supposed to be doing.
Great tips Trogluddite, thanks!
infuzion ·
trogluddite wrote: And never forget - the most important code in assembly is this one...
//Helpful comment so I can remember what the hell this code is doing
Once you find your feet and start optimising, you will find that variables get used less and less, and the "xmm's" get used more and more - and "xmm" isn't very helpful compared to, e.g. "VariableForThatReallyImportantThing".
Those double-slashes "//" are your bestest friend - without them it is sometimes hard to understand assembly that you wrote only a few hours ago!
Another little tip - it's quite rare to write directly in assembly; mostly people will check out their idea using code first. When I do this, I keep the original code in a small purgable module next to the assembly, so that it is always there to refer back to. As long as it isn't connected to anything, it won't use any CPU, and setting it to "purgable" means it will get deleted automatically when you export to VST etc. This has saved me many times when trying to de-bug a complex assembly routine - the old code tells me just what the assembly is supposed to be doing.
Comments are very important! I often use them to:
+ general info
+ track what variables the registers refer to
+ "comment-out" things I've eliminated, so I know I don't need them anymore
+ Code I'm editing in ASM in comments before the ASM. Sometimes it is just one line, but often it will be a entire block:
// Reset on retrigger or if no gate
//cnt = cnt&((env==0)|(env>1));
//off = off&((env==0)|(env>1));
//val = val&((env==0)|(env>1)) + val&(env==1)&smoothRetrig;
//val = val&(cnt>0 | smoothRetrig);
ASM here...
...because I will reduce this entire section in 1 sweep.
Sometimes I prefer to program directly in ASM without doing it in Code or primitives, if I can see the formulas in math in my head.
Chaoslala ·
Don't understand your codes !
One tips i like to do is to have a tick/mono/float module
and output every new variable i create so i can see if they
works fine.
infuzion ·
Chaoslala wrote: One tips I like to do is to have a tick/mono/float module and output every new variable I create so I can see if they work well.
This is a good tip! In fact I use a Stream Subtract connected to a Readout to ensure the old & new versions "null-out" (have the same results). It is also to check results very often (every few minutes) for optimizations, since small errors are more likely to become complicated & harder to find much later.
--
One non-optimization I find lately is people trying to save a value for a register by using it read-only, but will use extra registers anyway to do simple math. Example original:
...
addps xmm0,y; // store tmp in xmm0
movaps xmm1,k2;
mulps xmm1,xmm0;
addps xmm1,x;
movaps x,xmm1; // assign x
mulps xmm1,k1;
addps xmm1,xmm0; //xmm0 not used any more after here
Here it is more efficient to store the "tmp" xmm0 into a new register, then just read k2 in the mulps. This is how it will work on many CPUs:
addps xmm0,y; // extra cycle to read from y
movaps xmm1,k2; //must wait for memory read from y to end to begin new read
mulps xmm1,xmm0; //must wait for xmm1 to be loaded by movaps.
This is faster:
addps xmm0,y; //same as before
movaps xmm1,xmm0; //store tmp in xmm1; free movaps because...
mulps xmm0,k2; //will run at same time as movaps; can read & write here at nearly same cycles
Also, look for static & almost static values that you can combine. Eg in green:
(Hz * -Pi) / SampleRate
Hz may change often; it is connected to a knob. Since SR is almost static (set when program is loaded, or the rare occasion someone changes the Sample Rate), you can combine that divide with Pi like so:
Pi / SR = HeldValue // "Float Sample & Hold"
Hz * HeldValue // when Hz changes, we avoid the divide since it is pre-calculated
Note: Since Pi is a long fraction, & SR is a large value, if you try to null the values you will find Pi's rounding will cause a tiny fraction of a difference.
Example improvements below, original here: simple sine oscillator without using sin1()
trogluddite ·
infuzion wrote: Also, look for static & almost static values
And what could be more static than zero - it's even the same whether integer or float (all 32bits off). I don't know about anyone else, but boy do I seem to use zero a lot, especially in comparisons. Like all constants in ASM, you'd normally see it declared as a variable ("float F0=0"), meaning that you have to read a memory address just to get.. ...nothing!
Instead you can use this simple trick...
pslld xmm0,32; //Set xmm0 to zero
pslld simply shifts all of the bits in a number to the left, by the number of places you desire. Bits will "fall off" the left hand side, and 'off' bits get sucked into the right hand side to fill the empty space. So if you shift by 32 (or more) bits, it clears the whole register, to leave zero. All in one CPU cycle, and no memory read.
Here's something I have seen quite often that is well worth avoiding...
Repetitions of a comparison. For example, you might have several variables that need resetting when an input is triggered...
A = A + (B-A) & (Trigger>OldTrig);
C = C + (D-C) & (Trigger>OldTrig);
E = E + (F-E) & (Trigger>OldTrig);
So every line is having to read 'Trig' and "OldTrig" from memory (or the cache at least), and do the comparison.
The thing to remember here is that the mask created by the comparison is just a pattern of bits - it's not really a 'number' as such, but it can be stored in a variable (or register) just like any other value...
Test = Trigger > OldTrig;
A = A + (B-A) & Test;
C = C + (D-C) & Test;
E = E + (F-E) & Test;
Although it requires an extra variable in code, this is outweighed by losing a memory read and comparison for each of the duplicate lines. It also makes assembly optimising easier (where the boolean result can stay in a register, without needing a temporary variable).
infuzion ·
trogluddite wrote:
pslld xmm0,32; //Set xmm0 to zero
All in one CPU cycle, and no memory read.
Maybe faster, maybe not, & maybe slower.
You do get atleast one, if sometimes 2 free memory reads per opcode, esp if there are no memory reads before &/or after the read in question. Also, MOVAPS has its own "unfused domain" path (in Core2Duo+, & semi-true for AMD), so MOVAPS is free if another neighboring opcode is not trying to read from memory at same time). (For C2D+) PSLLD shares a path with MUL/DIV/SQRTPS & maybe AND/ANDN/ORPS, so PSLLD can cause the listed opcodes to be delayed. I don't want to write an essay; let Dr. Agner Fog school you 😉
trogluddite wrote: Here's something I have seen quite often that is well worth avoiding... Repetitions of a comparison. For example, you might have several variables that need resetting when an input is triggered...
A = A + (B-A) & (Trigger>OldTrig);
C = C + (D-C) & (Trigger>OldTrig);
E = E + (F-E) & (Trigger>OldTrig);
Are we optimizing SM's envelopes again? 😉
Yes, any math function calculated 2 times near each other MAY be helpful to pre-calculate. 3 times = USUALLY better. 4+ times = always.
trogluddite ·
infuzion wrote: You do get atleast one, if sometimes 2 free memory reads per opcode
I understand that other stuff can happen during the read - but surely, if you read a variable, that data has still got to be found from somewhere - worst case, the variable is not in cache and there is an unavoidable dependency chain - then that opcode and the dependencies have got to twiddle their thumbs for a long time.
Maybe I misunderstand something (not unusual!), but for a variable that has not been read for a while, I don't see how out-of-order processing is likely to trump the massive latency of a read from main memory unless you have a very long sequence of non-dependent opcodes.
infuzion wrote: let Dr. Agner Fog school you
Time for some revision, it's been a while since I read any of Agner's stuff - I think about 1% of it actually sinks in with each reading!
Keep up the good work of keeping me on my toes! 😀
infuzion ·
trogluddite wrote:
infuzion wrote: You do get atleast one, if sometimes 2 free memory reads per opcode
Maybe I misunderstand something (not unusual!), but for a variable that has not been read for a while, I don't see how out-of-order processing is likely to trump the massive latency of a read from main memory unless you have a very long sequence of non-dependent opcodes.
C2D have an excellent automatic out-of-order interpolation for opcodes. Also (I think unlike AMD chips) it will write to a different cache address in the following case:
mulps xmm1,xmm0;
movaps xmm0,FloatZero; //usually will execute at same time
Here, the second xmm0 is written to a different place than the old xmm0. Maybe that MOVAPS can be executed much earlier than the MULPS here?
trogluddite wrote:
infuzion wrote: let Dr. Agner Fog school you
Time for some revision, it's been a while since I read any of Agner's stuff - I think about 1% of it actually sinks in with each reading!
He is hard for me also 🙂 Look at what I just found:
Fog microarchitecture.pdf wrote:
; Example 2.1. Out of order processing
mov eax, [mem1]
imul eax, 5
add eax, [mem2]
mov [mem3], eax
Here, the ADD EAX,[MEM2] instruction is split into two µops. The advantage of this is that the microprocessor can fetch the value of [MEM2] at the same time as it is doing the multiplication. If none of the data are in the cache, then the microprocessor will start to fetch [MEM2] immediately after starting to fetch [MEM1], and long before the multiplication can start.
So even if the CPU has to make a RAM fetch, if there are no more reads long before that fetch, then it could still be "free", since for both Intel & AMD, fetching uses another "Execution pipe"/path that is not used by the maths.
So after 1 hour of researching & forum-posting, I guess we can conclude that
pslld xmm0,32; //Set xmm0 to zero
//WAS movaps xmm0,FloatZero;
should only be done after 2-4 rounds of ASM optimizing, when you find that you need that 0 value near other memory reads?
trogluddite ·
Thanks, that does seem clearer now. And I should at least have emphasised in my example that, as always, context is everything when deciding these things.
Mostly, I was making the assumption that, when running in conjunction with everything else in a typical music project, most codes would have most of their variables and opcodes flushed from the cache between iterations - such that even a single extra read of a 'cold' variable might be very costly. (especially if it caused a crucial cache line to be flushed.)
But with all this new fangled voodoo going on inside CPU's, I really do tie myself in knots trying to decide how justified such fears might be.
As ever, assumptions are not good science! I think that maybe many of my codes suffer from "new toys syndrome" - using freshly discovered techniques for the love of the "game" rather than good engineering!
infuzion ·
trogluddite wrote: But with all this new fangled voodoo going on inside CPU's, I really do tie myself in knots trying to decide how justified such fears might be.!
CPUs that have come out the past 3 years are smarter & come with more cache than before. Now you can spend some time & get alot of CPU reduced. If you want to get the last 10% reduced that is able to be, then you'll have to spend much much more time.
trogluddite ·
He he, maybe it is an alternative maxim of Moore's Law - every two years, CPUs get twice as difficult to optimise! 😉
infuzion ·
trogluddite wrote: He he, maybe it is an alternative maxim of Moore's Law - every two years, CPUs get twice as difficult to optimise! 😉
Actually they are easier in some ways, since the computer does alot of the work for interpolations now. But it often can not read your mind, so basic math reductions & watching dependance chains like the tip above are still helpful.
Wrote something here? To have a post removed or your name hidden, write to [email protected].