News:

Masm32 SDK description, downloads and other helpful links
Message to All Guests
NB: Posting URL's See here: Posted URL Change

Main Menu

Puzzled by FPU behavior (X87 newbie here)

Started by NoCforMe, December 28, 2021, 01:23:56 PM

Previous topic - Next topic

NoCforMe

Back after a long absence; time for more programming fun.

So I'm learning X87 programming. Just getting my feet wet. (I'm an experienced MASM/Win32 programmer.)

Here's what's puzzing me: in order to learn just basic stuff, like loading and storing data (and also the FXTRACT instruction, which intrigues me), I wrote this little snippet which simply loads pi, FXTRACTs it into significand and exponent, then displays these two values. Code is here:

FLDPI ;Load pi--> ST(0)
FXTRACT ;Exponent--> ST(0), sig.--> ST(1)
FSTP sig ;Significand--> sig

; Convert significand to ASCII text:
INVOKE FpuFLtoA, ADDR sig, $FPU_FL2AParms, ADDR buff1, SRC1_REAL or SRC1_DIMM or STR_REG

FSTP exp ;Exponent--> exp

; Convert exponent to ASCII text:
INVOKE FpuFLtoA, ADDR exp, $FPU_FL2AParms, ADDR buff2, SRC1_REAL or SRC1_DIMM or STR_REG

; Display results:
INVOKE wsprintf, ADDR buffer, OFFSET OutFmt, ADDR buff1, ADDR buff2
INVOKE SetWindowText, ExpHandle, ADDR buffer


(If someone wants I could provide the complete program, but I'd have to tweak it as I do some idiosyncratic things that won't work with most people's setups.)

Well, I get the oddest result: it works, sorta, but the significand displayed is exactly half of pi, 1.5707963268. Can't figure out why this is.

I would have expected to get 3.14159... for the significand and zero for the exponent, right? Instead, the exponent is 1. I guess that was naive of me.

I looked in an X87 manual I downloaded (from AMD), and here's what they say about the FXTRACT operation:

QuoteAfter this operation, the new ST(0) contains a real number with the sign and value of the original significand and an exponent of 3FFFh (biased value for true exponent of zero), and ST(1) contains a real number that is the value of the original value's true (unbiased) exponent.

I'm sorry to say I don't really understand what this means: I'm not sure about that biasing stuff, still have to do some more homework. Does this explain why I'm not seeing the results I thought I should get? How are you supposed to get a useful result out of FXTRACT? does the significand require further processing?

I'm thinking of using FXTRACT to write my own floating point-to-ASCII conversion routine, as an exercise. Oh, and if anyone's thinking of suggesting it, I'm definitely not interested in SSE or MMX; just want to learn plain old X87 (32-bit) for now.

Any help appreciated.
Assembly language programming should be fun. That's why I do it.

HSE

First thing, order is wrong. First fstp must be exp, second sig.
Second don't run functions between fpu operations if you can't check that functions don't modify fpu registers.
:thumbsup:
Equations in Assembly: SmplMath

NoCforMe

Quote from: HSE on December 28, 2021, 01:50:25 PM
First thing, order is wrong. First fstp must be exp, second sig.
Second don't run functions between fpu operations if you can't check that functions don't modify fpu registers.
:thumbsup:
Thanks for the reply. I changed the order of things to avoid that possibility:
FLDPI ;Load pi--> ST(0)
FXTRACT ;Exponent--> ST(0), sig.--> ST(1)
FSTP sig ;Significand--> sig
FSTP exp ;Exponent--> exp

; Convert significand to ASCII text:
INVOKE FpuFLtoA, ADDR sig, $FPU_FL2AParms, ADDR buff1, SRC1_REAL or SRC1_DIMM or STR_REG

; Convert exponent to ASCII text:
INVOKE FpuFLtoA, ADDR exp, $FPU_FL2AParms, ADDR buff2, SRC1_REAL or SRC1_DIMM or STR_REG

; Display results:
INVOKE wsprintf, ADDR buffer, OFFSET OutFmt, ADDR buff1, ADDR buff2
INVOKE SetWindowText, ExpHandle, ADDR buffer


But that's not the problem. (Ray Filiatreault was very careful when he wrote those library routines; they save and restore everything.) So that doesn't help explain my puzzlement here. I still get half of pi.
Assembly language programming should be fun. That's why I do it.

NoCforMe

Just so there's no question, here are the data items I didn't show so you can see those aren't the problem:


LOCAL exp:REAL10, sig:REAL10
LOCAL buffer[256]:BYTE, buff1[32]:BYTE, buff2[32]:BYTE
OutFmt DB "Significand: %s", 13, 10, "Exponent: %s", 0


And yeah, maybe I got the sig. & exp. mixed up; doesn't matter for now, I get the wrong # for the significand (obvious from the display which is which).
Assembly language programming should be fun. That's why I do it.

jj2007

I can't check your code because you don't supply a complete example, but have you thought of the implicit bit?

include \masm32\MasmBasic\MasmBasic.inc         ; download
  Init
  fldpi                                 ;load pi--> st(0)
  fxtract                               ;exponent--> st(0), sig.--> st(1)
  fmul FP4(2.0)
  fldpi
  deb 1, "test", ST(0), ST(1), ST(2)
EndOfCode


test
ST(0)           3.141592653589793238
ST(1)           3.141592653589793238
ST(2)           1.000000000000000000

raymond

QuoteWell, I get the oddest result: it works, sorta, but the significand displayed is exactly half of pi, 1.5707963268. Can't figure out why this is.

You must remember that the FPU works in base 2.

The result you are getting with the FXTRACT is the significand of 1.5707... and the exponent of 1.
When recombined in "scientific expression", you would get 1.5707... x 21.
Whenever you assume something, you risk being wrong half the time.
https://masm32.com/masmcode/rayfil/index.html

NoCforMe

Raymond--just the person I was hoping would comment on my perplexity! Thanks for that; will study and digest that.

So just to check, to get the correct answer, I would need to multiply the significand times the exponent times 2, is that correct?

sig X 2exp
Assembly language programming should be fun. That's why I do it.

raymond

Quote from: NoCforMe on December 29, 2021, 09:06:06 AM
Raymond--just the person I was hoping would comment on my perplexity! Thanks for that; will study and digest that.

So just to check, to get the correct answer, I would need to multiply the significand times the exponent times 2, is that correct?

sig X 2exp

Yes and no!!!!!

To be perfectly unambiguous, it's the significand multiplied by 2 to its reported exponent , i.e. sig X 2exp as you correctly indicated at the end,
but not multiplied by the exponent times 2 as stated in your text.

For example, if the given exponent would be 5, the significand would need to be multiplied by 25 (i.e. x 32) but not multiplied by (5 x 2).
Whenever you assume something, you risk being wrong half the time.
https://masm32.com/masmcode/rayfil/index.html

jj2007

Quote from: raymond on December 29, 2021, 07:23:12 AM
When recombined in "scientific expression", you would get 1.5707... x 21.

Thanks, Raymond. Why doesn't it use 3.14... x 20 then? Just curious...

raymond

Simply because you told the FPU to give you the "scientific binary notation" of the value in ST0 with the FXTRACT instruction.
It returns it as 1.xxxx and the xyz exponent of 2.

You may want to peek at the tutorial to see some of the potential uses of the results.
Whenever you assume something, you risk being wrong half the time.
https://masm32.com/masmcode/rayfil/index.html

NoCforMe

Is that the way FXTRACT always works, or can you change its behavior via mode-set bits?

Also, what's with that "exponent of 3FFFh (biased value for true exponent of zero)" business for the significand (what I copied out of the AMD manual)? Is that anything I need concern myself with or just an implementation detail I can ignore?

(And what tutorial is that? Somewhere on this site?)
Assembly language programming should be fun. That's why I do it.

raymond

QuoteIs that the way FXTRACT always works,

Yes.

The scientific notation of a number in any base B would always be A.xxxx....XBexp, with A being between 1 and (B-1).
Always being smaller than the base, the significand could then be displayed in scientific notation with its base having an exponent of 0, i.e. B0.

That is why the FPU returns the significand with its exponent set at 0 with the FXTRACT instruction. If you read the tutorial, you would learn that the exponent of the floating point format is biased to be able to care for both positive and negative ones. For REAL10s, the exponent is carried in 15 bits, with the last 14 bits, (i.e. 3FFFh) used as the bias, meaning 3FFFh would be for exponent 0. Anything smaller would be for negative exponents, anything larger for positive exponents.

The AMD manual was only trying to be thorough by mentioning that the exponent field for the returned value of the significand would be the biased value of 0 in the REAL10 format of the FPU registers.

Whenever you assume something, you risk being wrong half the time.
https://masm32.com/masmcode/rayfil/index.html

NoCforMe

So, thanks again. What I've learned so far is that the FPU forces the significand to be in the range 1<S<2. I'm still not sure why, but will continue studying this.

I wrote a little testbed program (attached here) to demonstrate (to myself) how this all works. Discovered that FSCALE is essentially (completely?) the inverse of FXTRACT. All this program does is show the result of splitting a number with FXTRACT and then "re-assembling" it with FSCALE.

So about this testbed program: So long as I'm posting it here, I thought I'd explain a little about it. It shows my coding style, which may be useful for anyone who's searching for their own style and is not sure how they want their code to look. Let me take pains to point out that I am not proposing this as the way everyone should write assembly language; far from it. If you're an experienced coder, you have no doubt developed the style that you prefer and don't need any advice from the likes of me. But if you're just starting out, there may be some useful things in my style for you to borrow.

Here's what I do:
I prefer to write ASM statements in caps, with variable names in mixed case (upper and lower):

;============= Message loop ===================
msgloop:
INVOKE GetMessage, ADDR msg, NULL, 0, 0
OR EAX, EAX ;EAX = 0 = exit
JZ exit99
INVOKE TranslateMessage, ADDR msg
INVOKE DispatchMessage, ADDR msg
JMP msgloop


I guess to me that looks more "assembler-ish", and easier to read.

Variable names:
I distinguish between global variables, which I start with a capital letter, and local variables, which start lowercase:

MOV EAX, lParam ;lParam is a local (actually a function argument)
CMP EAX, SigRawOutHandle ;SigRawOutHandle is a global


and a third type of identifier is a constant (an EQU-defined number):

MOV EDX, $mainWinWidth

where this constant is defined thus:

$mainWinWidth EQU 370

To me, this really helps to figure out what kind of "object" I'm dealing with, whether it's a local variable, a global or a defined constant (not a variable at all).

Which brings us to EQUs in general; use them, instead of sticking constants (numbers) all over the place in your code. Two reasons: one, they give you an idea of what they mean when you read them instead of some possibly meaningless number, and two, if it's a constant that's used in more than one place and you ever need to change it, it's much easier to just change the EQU rather than having to hunt down every instance of the number.

Comments: Use them!
I know some of us feel like we're too cool to comment our own code. I've read some of this code: it sucks! Maybe it works now, and maybe you understand it now, but try coming back to it in five or ten years and see if you really understand that really clever thing you did with bit-twiddling or whatever. The person that comments will help is you. At least that's my experience. You don't have to comment each and every line of code, but do put comments wherever something might not be obvious to whoever's reading it (which, as I pointed out, might be you some years from now).

And something I really like to do in code is make it data-driven by putting stuff into data structures rather than code. In this li'l program, it's how all the control windows (static, edit, button) get created. In the data segment, there's a structure that contains all the parameters for all these controls:

ControlParams LABEL DWORD
$CWP <20, 22, 40, 12, $PiRadio, ButtonClassName, PiRadioTxt, $radioStyles_GROUP, NULL, PiRadioHandle>
$CWP <60, 22, 60, 12, $NumRadio, ButtonClassName, NumRadioTxt, $radioStyles, NULL, NumRadioHandle>
$CWP <120, 19, 80, 18, $NumEdit, EditClassName, NULL, $editStyles, NULL, NumEditHandle>
$CWP <245, 18, 40, 20, IDOK, ButtonClassName, GoBtnTxt, $buttonStyles, NULL, GoBtnHandle>
$CWP <120, 60, 150, 16, 100, StaticClassName, ResultsTxt, $staticStyles, NULL, NULL>
$CWP <90, 80, 60, 16, 101, StaticClassName, RawTxt, $staticStyles, NULL, NULL>
$CWP <240, 80, 45, 16, 102, StaticClassName, DecTxt, $staticStyles, NULL, NULL>
$CWP <20, 100, 20, 16, 103, StaticClassName, SigTxt, $staticStyles, NULL, NULL>
$CWP <45, 100, 120, 16, $sigRawOut, StaticClassName, NULL, $staticStyles, NULL, SigRawOutHandle>
$CWP <190, 100, 20, 16, 104, StaticClassName, SigTxt, $staticStyles, NULL, NULL>
$CWP <215, 100, 120, 16, $sigDecOut, StaticClassName, NULL, $staticStyles, NULL, SigDecOutHandle>
$CWP <20, 120, 20, 16, 105, StaticClassName, ExpTxt, $staticStyles, NULL, NULL>
$CWP <45, 120, 120, 16, $expRawOut, StaticClassName, NULL, $staticStyles, NULL, ExpRawOutHandle>

DD -1 ;End of list marker


Then when it's time to create these controls, all that's needed is:

; Create control windows from control-parms struct:
PUSH EBX
LEA EBX, ControlParams

nxtwin: INVOKE CreateWindowEx,
0, ;WS_EX_xxx styles
[EBX].$CWP.classNamePtr, ;ptr. to Win32 class name
[EBX].$CWP.textPtr, ;ptr. to window name/text
[EBX].$CWP.styles, ;WS_xxx styles
[EBX].$CWP.x, ;X-position
[EBX].$CWP.y, ;Y-position
[EBX].$CWP.wdth, ;width
[EBX].$CWP.hght, ;height
MainWinHandle, ;parent window
[EBX].$CWP.ID, ;Menu/ID
hInst, ;Instance handle
NULL ;lParam pointer (not used)

; Move to next item in list:
ADD EBX, SIZEOF $CWP
CMP [EBX].$CWP.x, -1 ;End o'list?
JNZ nxtwin
POP EBX


See how simple that is? The alternative would be many, many CreateWindowEx() blocks, each with all those arguments. Not fun!

By the way, the program shows how to create a "dialog" without an actual dialog, and without using the resource compiler. I did this since this was just a quick-and-dirty app. It took maybe an hour or so to line everything up nicely (I used WinSpy to select each control and actually move it on-screen until it was where I wanted it). I don't recommend this technique for any kind of production code, but this shows that it can be done if you want.

Last thing: I put a little "extra credit" stuff in which is the code that shows the result fields (which are just static controls that I set with SetWindowText() ) in red. This is actually pretty simple: in your main window procedure, you look for the message WM_CTLCOLORSTATIC (which gets sent to the parent window of the control), and when you receive it:

;*****************************
; WM_CTLCOLORSTATIC handler
;*****************************

; Look for handles of controls where we want red text.
; lParam = handle of control that sent message:
doRed: MOV EAX, lParam
CMP EAX, SigRawOutHandle
JE @F
CMP EAX, ExpRawOutHandle
JE @F
CMP EAX, SigDecOutHandle
JNE common_exit

@@: INVOKE SetTextColor, wParam, $colorRed ;wParam = HDC.
INVOKE SetBkColor, wParam, BkColor

; Return with background brush:
MOV EAX, BkBrush
RET


Windows conveniently sends you two handles with that message, in lParam and wParam: one is the handle of the control sending the message, and the other is a handle to its device context (HDC) which you can use to manipulate the display colors.

Anyhow, hope some of this might be of help to someone out there.
Assembly language programming should be fun. That's why I do it.