Page 1 of 2

Fastest way to get the decimals of a float?

Posted: Fri Nov 14, 2008 11:03 am
by superadnim
How can I get the decimals of a float variable, the fastest way? (I don't mind ASM)

Here is what I'm currently doing through an equivalent of type casting

Code: Select all

Macro decimals( _n_ ) : (_n_-Int(_n_)) : EndMacro

Define.f temp = 12.34

Debug temp
Debug decimals(temp)
I know, horrible... but that's the best I could come up with, any ideas?

Posted: Fri Nov 14, 2008 11:38 am
by Little John
It's pretty fine IMHO, not horrible. :-)

However, in order to work for big numbers in PureBasic versions < 4.30, I think it should look like this:

Code: Select all

Macro decimalsQ( _n_ ) : (_n_-IntQ(_n_)) : EndMacro

Define.f temp = 12.34

Debug temp
Debug decimalsQ(temp)
Regards, Little John

Posted: Fri Nov 14, 2008 1:10 pm
by dioxin
I don't mind ASM

Code: Select all

!push &h1f7f0000       'the FP control word needed to round to zero
!fstcw [esp]           'save the current FP control word
!fldcw [esp+2]         'set the FP to round to zero
!fld MyNumber          'load the FP with the number to work on
!fld st(0)             'replicate that number on the stack
!frndint               'round number to leave integer
!fsubp st(1),st(0)     'subtract from original to leave fraction part (and pop stack)
!fstp MyNumber         'store result (and pop stack, FP stack is now clean)
!fldcw [esp]           'restore the original control word (in case other code needs it)
!add esp,4             'clean up CPU stack
Depending on circumstances, there might be quicker ways.

Posted: Fri Nov 14, 2008 6:05 pm
by akj
@dioxin:

I have a fairly poor knowledge of how to implement assembler routines in PureBasic, but nevertheless I have attempted to convert your routine to work as a PB procedure, but it always returns a result of 0.0 .
What am I doing wrong?

Code: Select all

Procedure.d Frac(MyNumber.d)
; Return fractional part of FP number
Protected result.d
!push $1f7f0000      ; FP control word needed to round to zero
!fstcw [esp]         ; Save the current FP control word
!fldcw [esp+2]       ; Set the FP to round to zero
!fld qword [p.v_MyNumber] ; Load the FP with the number to work on
!fld st0             ; Replicate/push that number on the FP stack
!frndint             ; Round number to leave integer
!fsubp st1,st0       ; Subtract from original to leave fraction part (and pop stack)
!fstp qword [p.v_result] ; Store result (and pop stack, FP stack is now clean)
!fldcw [esp]         ; Restore original FP control word
!add esp,4           ; Clean up CPU stack
ProcedureReturn result
EndProcedure

Define.d temp = 12.345
Debug temp
Debug Frac(temp)

Posted: Fri Nov 14, 2008 6:43 pm
by Fred
As you modify the stack register, PB can't know about it, so you have to adjust your variable access according to it:

Code: Select all

Procedure.d Frac(MyNumber.d)
; Return fractional part of FP number
Protected result.d
!push $1f7f0000      ; FP control word needed to round to zero
!fstcw [esp]         ; Save the current FP control word
!fldcw [esp+2]       ; Set the FP to round to zero
!fld qword [p.v_MyNumber+4] ; Load the FP with the number to work on
!fld st0             ; Replicate/push that number on the FP stack
!frndint             ; Round number to leave integer
!fsubp st1,st0       ; Subtract from original to leave fraction part (and pop stack)
!fstp qword [p.v_result+4] ; Store result (and pop stack, FP stack is now clean)
!fldcw [esp]         ; Restore original FP control word
!add esp,4           ; Clean up CPU stack
ProcedureReturn result
EndProcedure

Define.d temp = 12.345
Debug temp
Debug Frac(temp)
You can also avoid the temporary result, by omitting the variable to procedurereturn (then the ST1 result will be returned):

Code: Select all

Procedure.d Frac(MyNumber.d)
; Return fractional part of FP number
!push $1f7f0000      ; FP control word needed to round to zero
!fstcw [esp]         ; Save the current FP control word
!fldcw [esp+2]       ; Set the FP to round to zero
!fld qword [p.v_MyNumber+4] ; Load the FP with the number to work on
!fld st0             ; Replicate/push that number on the FP stack
!frndint             ; Round number to leave integer
!fsubp st1,st0       ; Subtract from original to leave fraction part (and pop stack)
!fldcw [esp]         ; Restore original FP control word
!add esp,4           ; Clean up CPU stack
ProcedureReturn
EndProcedure

Define.d temp = 12.345
Debug temp
Debug Frac(temp)

Posted: Fri Nov 14, 2008 7:19 pm
by akj
@Fred:

Thank you for your very helpful and clear reply.

I had a feeling that ProcedureReturn (without any argument) would in this case return the contents of st0 as I think I noticed it mentioned somewhere on the forum. However it does not appear to be officially documented anywhere when the return type is non-integer.

Posted: Fri Nov 14, 2008 8:55 pm
by Fred
My mistake, it's ST0 indeed. If you return type is an integer then it's eax.

Posted: Fri Nov 14, 2008 9:56 pm
by akj
What is returned by ProcedureReturn (without any argument) when the procedure is defined to return a string?

Posted: Fri Nov 14, 2008 10:39 pm
by Fred
Nothing, as the return value isn't used.

Posted: Sat Nov 15, 2008 1:38 pm
by superadnim
thanks :) that's about 20% faster than my macro!

even with the doubles :? I wonder, if I wanted single precision, would changing qword to dword and the return type to float (as well as the parameter type) fix it?

now that I think about it, is 20% really negligible?. right now I doubt any bottleneck would form around the current macro and quite frankly using asm will only lead to maintenance issues in the future :?

but im all for speed :lol:

Posted: Sat Nov 15, 2008 1:48 pm
by Fred
Depending of what you do, but yes, 20% isn't much for an asm replacement.

Posted: Sat Nov 15, 2008 1:54 pm
by dioxin
Superadnim,
I'd have expected better than 20% improvement, it runs in about 30clks on my PC. Have you tried using a macro for the ASM function instead of calling it in a procedure? For very short code like this the overhead of calling the procedure becomes significant.

Posted: Sat Nov 15, 2008 2:00 pm
by superadnim
But isn't Int() a procedure? or is it expanded inlined by the compiler? (not sure, that's why I'm asking)

if it's being inlined then I'll try turning the current asm solution into a macro to make the test more fair

Posted: Sat Nov 15, 2008 2:12 pm
by superadnim

Code: Select all

Macro decimals2( _n_, _result_ )
	
	!push $1f7f0000      ; FP control word needed to round to zero 
	!fstcw [esp]         ; Save the current FP control word 
	!fldcw [esp+2]       ; Set the FP to round to zero 
	!fld dword[v_#_n_] ; Load the FP with the number to work on 
	!fld st0             ; Replicate/push that number on the FP stack 
	!frndint             ; Round number to leave integer 
	!fsubp st1,st0       ; Subtract from original to leave fraction part (and pop stack) 
	!fldcw [esp]         ; Restore original FP control word 
	!fstp dword[v_#_result_]
	!add esp,4           ; Clean up CPU stack 
	
EndMacro
I don't know if I should store the value after the add or not but it works anyway I think.

with the macro I got 29% compared to the previous 20% against the int macro :?

10000000 iterations with the process priority set to realtime and with a delay between each test, the result is an average of 6 tests but in this case they all clocked the same 20% and 29% :P

Posted: Sat Nov 15, 2008 3:14 pm
by dioxin
superadnim,
It doesn't matter if you store the value before or after that final ADD.

Something doesn't add up. Looking at the ASM code produced by the compiler for your original macro (_n_-Int(_n_)) there's no way that the ASM I posted above should be only 29% faster.
How long does your 10,000,000 iterations take in seconds? Here it only takes 0.25s which works out at around 40 cpu clks per iteration, about what I'd expect.
Have you turned off the debugger before timing it?