- Joined
- Dec 12, 2012
- Messages
- 1,007
real
Talk - Floats in Warcraft 31. Introduction
In Warcraft 3 there exist almost 100 different native datatypes. Most of those datatypes extend of
handle
, which can be seen as "base-class" of the Jass type system. Apart from those, there exist seven basic types:-
nothing
The incomplete type for denoting "no type" -
boolean
A simple true/false datatype -
integer
The standard 32 bit integer datatype -
real
A 32 bit floating point datatype -
string
A charakter array for representing text -
code
A function pointer for a function that takes no arguments -
handle
The base of all further derived types
For most of these types the usage is pretty clear and they basically work "as expected". There are some limitations on
string
that it can't grow infinitly or that you can't make arrays of type code
, but those are quite understandable and straightforward. The only exception to this is the datatype real
. There exist probably more rumors and wrong/misleading information about this datatype than for all of the other six basic types together. I have read things like 0.001 is the smallest real in Wc3, that long reals are interpreted as strings and many more.This is especially remarkable since the most fundamental things in Wc3 are modelled with reals: Unit health, mana, damage and armor, just to mention a few of them. Since the real datatype is so important and widely used in Wc3, I think its time to collect the most important things about it in one article which can serve as future reference. During some investigation about reals in Wc3 I found some interesting points I want to share, discuss and hopefully extend by further ideas and findings by other users. Once this is done, the goal is to publish this as a tutorial that covers everything relevant about the real datatype.
The findings and/or conclusions might be relevant for building the first robust
UNIT_STATE_LIFE
/UNIT_STATE_MANA
library in Wc3.2. Reals in General
Standard floating point datatypes (in many languages called float when refering to single precision) already can cause quite some trouble when programming if you don't know about the exact behavior of floats. There exist great tutorials on this topic, but there are quite some things one has to consider when working with reals and accuracy matters.
To make things worse, reals in Wc3 behave in quite some points different than the IEEE 754 standard. This article therefore won't focus on explaining floating point numbers in detail, since there already exist quite massive literature and references on this topic, but on the differences and pitfalls with floats in Jass compared to most other programming/scripting languages.
3. Reals in Warcraft 3
3.1 Operator accuracy
First lets consider the standard algorithm to calculate the smallest positive floating point number that can be represented by the corresponding datatype. For all reference examples, standard C++ is used.
C++:
#include <iostream>
#include <limits>
int main()
{
std::cout.precision(std::numeric_limits<float>::max_digits10);
int i = 0;
float f = 1.0f;
while (f != 0.0f)
{
std::cout << i++ << '\t' << f << '\n';
f /= 2.0f;
}
}
Code:
0 1
1 0.5
2 0.25
3 0.125
4 0.0625
5 0.03125
6 0.015625
7 0.0078125
8 0.00390625
9 0.001953125
10 0.0009765625
11 0.00048828125
12 0.000244140625
13 0.000122070313
14 6.10351563e-005
15 3.05175781e-005
16 1.52587891e-005
17 7.62939453e-006
18 3.81469727e-006
19 1.90734863e-006
20 9.53674316e-007
21 4.76837158e-007
22 2.38418579e-007
23 1.1920929e-007
24 5.96046448e-008
25 2.98023224e-008
26 1.49011612e-008
27 7.4505806e-009
28 3.7252903e-009
29 1.86264515e-009
30 9.31322575e-010
31 4.65661287e-010
32 2.32830644e-010
33 1.16415322e-010
34 5.82076609e-011
35 2.91038305e-011
36 1.45519152e-011
37 7.27595761e-012
38 3.63797881e-012
39 1.8189894e-012
40 9.09494702e-013
41 4.54747351e-013
42 2.27373675e-013
43 1.13686838e-013
44 5.68434189e-014
45 2.84217094e-014
46 1.42108547e-014
47 7.10542736e-015
48 3.55271368e-015
49 1.77635684e-015
50 8.8817842e-016
51 4.4408921e-016
52 2.22044605e-016
53 1.11022302e-016
54 5.55111512e-017
55 2.77555756e-017
56 1.38777878e-017
57 6.9388939e-018
58 3.46944695e-018
59 1.73472348e-018
60 8.67361738e-019
61 4.33680869e-019
62 2.16840434e-019
63 1.08420217e-019
64 5.42101086e-020
65 2.71050543e-020
66 1.35525272e-020
67 6.77626358e-021
68 3.38813179e-021
69 1.69406589e-021
70 8.47032947e-022
71 4.23516474e-022
72 2.11758237e-022
73 1.05879118e-022
74 5.29395592e-023
75 2.64697796e-023
76 1.32348898e-023
77 6.6174449e-024
78 3.30872245e-024
79 1.65436123e-024
80 8.27180613e-025
81 4.13590306e-025
82 2.06795153e-025
83 1.03397577e-025
84 5.16987883e-026
85 2.58493941e-026
86 1.29246971e-026
87 6.46234854e-027
88 3.23117427e-027
89 1.61558713e-027
90 8.07793567e-028
91 4.03896783e-028
92 2.01948392e-028
93 1.00974196e-028
94 5.04870979e-029
95 2.5243549e-029
96 1.26217745e-029
97 6.31088724e-030
98 3.15544362e-030
99 1.57772181e-030
100 7.88860905e-031
101 3.94430453e-031
102 1.97215226e-031
103 9.86076132e-032
104 4.93038066e-032
105 2.46519033e-032
106 1.23259516e-032
107 6.16297582e-033
108 3.08148791e-033
109 1.54074396e-033
110 7.70371978e-034
111 3.85185989e-034
112 1.92592994e-034
113 9.62964972e-035
114 4.81482486e-035
115 2.40741243e-035
116 1.20370622e-035
117 6.01853108e-036
118 3.00926554e-036
119 1.50463277e-036
120 7.52316385e-037
121 3.76158192e-037
122 1.88079096e-037
123 9.40395481e-038
124 4.7019774e-038
125 2.3509887e-038
126 1.17549435e-038
127 5.87747175e-039
128 2.93873588e-039
129 1.46936794e-039
130 7.34683969e-040
131 3.67341985e-040
132 1.83670992e-040
133 9.18354962e-041
134 4.59177481e-041
135 2.2958874e-041
136 1.1479437e-041
137 5.73971851e-042
138 2.86985925e-042
139 1.43492963e-042
140 7.17464814e-043
141 3.58732407e-043
142 1.79366203e-043
143 8.96831017e-044
144 4.48415509e-044
145 2.24207754e-044
146 1.12103877e-044
147 5.60519386e-045
148 2.80259693e-045
149 1.40129846e-045
As one can see, the algorithm stops after 150 iterations and yields the smallest positive floating point number representable with 32 bit, namely 1.40129846e-045. This value is not "normal", but I will come to this later.
First lets try out the same thing in vJass, if we get identical results. If Wc3 reals are 32 bit floats, we should get identical results. So here is a possible implementation and output of the above algorithm in vJass:
JASS:
scope RealTest initializer onInit
private function onInit takes nothing returns nothing
local real r = 1.0
local integer i = 0
loop
call BJDebugMsg(I2S(i) + " " + R2SW(r, 50, 50))
set r = r/2.0
set i = i + 1
exitwhen r == 0.0
endloop
call BJDebugMsg("Finished") // Just to make sure we didn't hit the OP limit
endfunction
endscope
Code:
0 1
1 0.5
2 0.25
3 0.125
4 0.0625
5 0.03125
6 0.015625
7 0.0078125
8 0.00390625
9 0.001953125
Finished
Now that was unexpected!
The same algorithm produces a very different output and terminates after 10 iterations leading to a minimum float value of 0.001953125. The question is now, why is that? Is the real datatype a 32 float at all if it behaves that different? If not, what is it? And why does it terminate at such a strange value like 0.001953125?
To answer those questions, we rearrange the vJass algorithm a bit:
JASS:
scope RealTest initializer onInit
private function onInit takes nothing returns nothing
local real r = 1.0
local integer i = 0
loop
call BJDebugMsg(I2S(i) + " " + R2SW(r, 50, 50))
set r = r/2.0
set i = i + 1
exitwhen not (r != 0.0) // <- Only difference here
endloop
call BJDebugMsg("Finished") // Just to make sure we didn't hit the OP limit
endfunction
endscope
Code:
0 1.000000000
1 0.500000000
2 0.250000000
3 0.125000000
4 0.062500000
5 0.031250000
6 0.015625000
7 0.007812500
8 0.003906250
9 0.001953125
10 0.000976563
11 0.000488281
12 0.000244141
13 0.000122070
14 0.000061035
15 0.000030518
16 0.000015259
17 0.000007629
18 0.000003815
19 0.000001907
20 0.000000954
21 0.000000477
22 0.000000238
23 0.000000119
24 0.000000060
25 0.000000030
26 0.000000015
27 0.000000007
28 0.000000004
29 0.000000002
30 0.000000001
31 0.000000000
32 0.000000000
33 0.000000000
34 0.000000000
35 0.000000000
36 0.000000000
37 0.000000000
38 0.000000000
39 0.000000000
40 0.000000000
41 0.000000000
42 0.000000000
43 0.000000000
44 0.000000000
45 0.000000000
46 0.000000000
47 0.000000000
48 0.000000000
49 0.000000000
50 0.000000000
51 0.000000000
52 0.000000000
53 0.000000000
54 0.000000000
55 0.000000000
56 0.000000000
57 0.000000000
58 0.000000000
59 0.000000000
60 0.000000000
61 0.000000000
62 0.000000000
63 0.000000000
64 0.000000000
65 0.000000000
66 0.000000000
67 0.000000000
68 0.000000000
69 0.000000000
70 0.000000000
71 0.000000000
72 0.000000000
73 0.000000000
74 0.000000000
75 0.000000000
76 0.000000000
77 0.000000000
78 0.000000000
79 0.000000000
80 0.000000000
81 0.000000000
82 0.000000000
83 0.000000000
84 0.000000000
85 0.000000000
86 0.000000000
87 0.000000000
88 0.000000000
89 0.000000000
90 0.000000000
91 0.000000000
92 0.000000000
93 0.000000000
94 0.000000000
95 0.000000000
96 0.000000000
97 0.000000000
98 0.000000000
99 0.000000000
100 0.000000000
101 0.000000000
102 0.000000000
103 0.000000000
104 0.000000000
105 0.000000000
106 0.000000000
107 0.000000000
108 0.000000000
109 0.000000000
110 0.000000000
111 0.000000000
112 0.000000000
113 0.000000000
114 0.000000000
115 0.000000000
116 0.000000000
117 0.000000000
118 0.000000000
119 0.000000000
120 0.000000000
121 0.000000000
122 0.000000000
123 0.000000000
124 0.000000000
125 0.000000000
126 0.000000000
Finished
This looks much better already!
There are two things immediately attracting attention. First, we get visible results until 0.000000001, which is the smallest positive number that can be displayed with
R2SW
. Since Wc3 doesn't support scientific notation, all numbers smaller than this number will be displayed as 0.000000000, even though they are not zero.Second, and much more important, the number of iterations and accuracy of the real datatye suddenly increased significantly. The only thing we changed is
operator==
to not operator!=
which should be logically equivalent, but in fact are not in Jass. It seems Blizzard wanted to make things "easier" for people when working with reals and therefore implemented the operator==
for reals with some epsilon. However, they only did so for this operator, not for the other comparison operators like operator!=
. This property was first reported by masda70 and is quantified here. I.e. the epsilon used by operator==
must be somewhere around 0.001. So the first conclusion we can draw here is:1 Conclusion: If you need maximum accuracy when comparing two reals for equality, use
not operator!=
instead of operator==
. All other comparison operators also have the usual accuracy and might safely be used.But lets proceed.
3.2 Normals and Denormals
You might remember me talking about "normal" numbers before. Also there is still something strange: Even with the usage of the
not !=operator
, we still get different results with our vJass algorithm compared to the C++ algorithm. The vJass version terminates after 127 steps, while the C++ version terminates after 150 steps. So is a C++ float still more accurate than a Jass real? The answer is yes and no.One important thing to notice is, that in C++ typically denormal numbers are used. Denormal numbers are smaller than the smallest "normal" floating point value and add aditional accuracy to the floating point type by using a trick in their representation. While denormal numbers have advantage that the invariant
x == y
<-> x - y == 0
always holds, they also are notorious for affecting performance in a negative way.Since Blizzard didn't seem to care about so small values (rounding with operator==, no scientific notation etc.), it makes sense that they disabled denormals in Wc3. We can do the same in C++ by modifying our test program slightly:
C++:
#include <xmmintrin.h>
#include <pmmintrin.h>
#include <iostream>
#include <limits>
int main()
{
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); // Disable denormals
std::cout.precision(std::numeric_limits<float>::max_digits10);
int i = 0;
float f = 1.0f;
while (f != 0.0f)
{
std::cout << i++ << '\t' << f << '\n';
f /= 2.0f;
}
}
Code:
0 1
1 0.5
2 0.25
3 0.125
4 0.0625
5 0.03125
6 0.015625
7 0.0078125
8 0.00390625
9 0.001953125
10 0.0009765625
11 0.00048828125
12 0.000244140625
13 0.000122070313
14 6.10351563e-005
15 3.05175781e-005
16 1.52587891e-005
17 7.62939453e-006
18 3.81469727e-006
19 1.90734863e-006
20 9.53674316e-007
21 4.76837158e-007
22 2.38418579e-007
23 1.1920929e-007
24 5.96046448e-008
25 2.98023224e-008
26 1.49011612e-008
27 7.4505806e-009
28 3.7252903e-009
29 1.86264515e-009
30 9.31322575e-010
31 4.65661287e-010
32 2.32830644e-010
33 1.16415322e-010
34 5.82076609e-011
35 2.91038305e-011
36 1.45519152e-011
37 7.27595761e-012
38 3.63797881e-012
39 1.8189894e-012
40 9.09494702e-013
41 4.54747351e-013
42 2.27373675e-013
43 1.13686838e-013
44 5.68434189e-014
45 2.84217094e-014
46 1.42108547e-014
47 7.10542736e-015
48 3.55271368e-015
49 1.77635684e-015
50 8.8817842e-016
51 4.4408921e-016
52 2.22044605e-016
53 1.11022302e-016
54 5.55111512e-017
55 2.77555756e-017
56 1.38777878e-017
57 6.9388939e-018
58 3.46944695e-018
59 1.73472348e-018
60 8.67361738e-019
61 4.33680869e-019
62 2.16840434e-019
63 1.08420217e-019
64 5.42101086e-020
65 2.71050543e-020
66 1.35525272e-020
67 6.77626358e-021
68 3.38813179e-021
69 1.69406589e-021
70 8.47032947e-022
71 4.23516474e-022
72 2.11758237e-022
73 1.05879118e-022
74 5.29395592e-023
75 2.64697796e-023
76 1.32348898e-023
77 6.6174449e-024
78 3.30872245e-024
79 1.65436123e-024
80 8.27180613e-025
81 4.13590306e-025
82 2.06795153e-025
83 1.03397577e-025
84 5.16987883e-026
85 2.58493941e-026
86 1.29246971e-026
87 6.46234854e-027
88 3.23117427e-027
89 1.61558713e-027
90 8.07793567e-028
91 4.03896783e-028
92 2.01948392e-028
93 1.00974196e-028
94 5.04870979e-029
95 2.5243549e-029
96 1.26217745e-029
97 6.31088724e-030
98 3.15544362e-030
99 1.57772181e-030
100 7.88860905e-031
101 3.94430453e-031
102 1.97215226e-031
103 9.86076132e-032
104 4.93038066e-032
105 2.46519033e-032
106 1.23259516e-032
107 6.16297582e-033
108 3.08148791e-033
109 1.54074396e-033
110 7.70371978e-034
111 3.85185989e-034
112 1.92592994e-034
113 9.62964972e-035
114 4.81482486e-035
115 2.40741243e-035
116 1.20370622e-035
117 6.01853108e-036
118 3.00926554e-036
119 1.50463277e-036
120 7.52316385e-037
121 3.76158192e-037
122 1.88079096e-037
123 9.40395481e-038
124 4.7019774e-038
125 2.3509887e-038
126 1.17549435e-038
And there you go, we get exactly the same output with out C++ program like with our vJass program. The algorithms terminate after 127 iterations and yield the smallest positive float number, 1.17549435e-038 which also equals
std::numeric_limits<float>::min()
. This can be seen as a prove for our second conclusion about reals in Wc3:2 Conclusion: The Warcraft 3 real datatype is a 32 bit float with disabled denormal numbers.
3.3 Inf, NaN and 1.0e9
The IEEE Standard also defines Inf (Infinity) and NaN (Not a Number). For example the result of the division 1/0 is defined as positive infinty, while the division 0/0 for example yields a NaN.
Warcraft 3 behaves quite different here. A NaN doesn't seem to exist at all, since division by zero causes a thread crash and therefore termination of the function trying to compute a NaN. More interesting, when looking at the object editor it seems that reals are capped at 1.0e9, because even with holding shift it is not possible to enter bigger values in object fields. But the real datatype is capable of storing bigger numbers (exactly like the float datatype in C++), so this seems to be related exclusivly to the object editor. Even the Inf value does exist (computalbe for example with the Pow function), it is even displayed correctly:
JASS:
call BJDebugMsg(R2S(Pow(2.0, 128.0))) // Displays: inf
However, it does not behave like an Inf. In especially, every attempt to decrease an inf value should either result in inf, -inf or NaN. However, multiplying a Wc3 inf with 2 will result in 0. So this is definitly not the expected behavior and therefore leads to our next conclusion:
3 Conclusion: Warcraft 3 reals cannot be NaN. They can be much larger than 1.0e9, i.e. up to 1.70141183e+038 (the biggest standard float) and even inf. However, the value inf doesn't behave like an IEEE 754 inf but like a "normal" value.
3.4 Minimum Unit Life and the problem with literals
Another interesting question is, what is the minimum health value a unit can have without dying. While this is a quite important question for a game like Wc3 and it would be quite intuitive that this boarder is marked by zero, in Wc3 things are different. It is well known that units can't survive with, for example 0.2 health and it seems the health boarder is around 0.406 which is also widely used in scripts. However, it turned out this constant is not accurate. There are quite some scripts that rely on the fact that
GetWidgetLife(u) < 0.406
and even though chances are very low that this won't be fullfilled during normal gameplay, there is the chance of having a bug due to this. Using the absolute correct value would erase this chance.So one could simply come to the idea to decrease the value 0.406 (maybe combined with a binary search) to get the smallest possible health value. However, there is a problem with this approach. I.e. you always have to consider the current spacing of the floating point value. Floats are not evenly spaced like integers (where the absolute difference between two successive integers is always 1). When dealing with floats, this difference depends on the magnitude of the float itself.
You might be familiar with this phenomenon from the object editor. If you set a units health to the maximum object editor value (1000000000 == 1.0e9), you are within a region where two floats might differ by at minimum a value of 64. So when you try to enter for example a value of 999999950, you will in fact get a unit with 999999936 health. This is because the prior float of 1000000000 is 999999936, which differ by 64. So any attempt to use values in between of 999999936 and 1000000000 will be either rounded up or down towards the closest representative floating point value. Since 999999950 is closer to 999999936, it will be rounded down.
The same problem applies to floats in Jass. For example a value like 0.405014016 is not directly representable as a float and is rounded to 0.405014008. Now operating on the last digit is very dangerous, because it is in the order of magnitude of 1.0e-9. However, floats within the size of 0.405014008 have a minimum difference of 2.98023224e-8, which is almost 30 times larger. So operating on the last digit will lead to rounding towards next representable floats and might skip several values without the user even noticing.
The only save way to do this is to use some function that can compute the prior representable float with exact precision. In Wc3 such a function does not exist, so we have to use C++ once again:
C++:
#include <xmmintrin.h>
#include <pmmintrin.h>
#include <iostream>
#include <limits>
#include <boost\math\special_functions\next.hpp>
int main()
{
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); // Disable denormals
std::cout.precision(std::numeric_limits<float>::max_digits10);
float f = 0.406f;
// Now decrease f using float_prior from boost and test the value until the unit dies
std::cout << boost::math::float_prior(f) << '\n';
}
This code can be combined with a binary search. I have done this and can now present the absolute correct values for units minimum life:
JASS:
call SetWidgetLife(u, 0.404998779) // Unit alive
call SetWidgetLife(u, 0.404998749) // Unit dead
// Difference: 2.98023224e-8
You can also use an online IEEE converter to compute the next representable floating value. Just make sure to use 9 significant digits (which equals
std::numeric_limits<float>::max_digits10
and is neccessary for perfect serialisation accuracy). This leads us to our forth conclusion:4 Conclusion: A units minimum life is exactly 0.404998779. The prior representable float is 0.404998749 (which has a difference of 2.98023224e-8 to the former one) and is the largest value which makes a unit die.
4. Outlook and Todo
There is still a lot of research to be done and experiments to be carried out. Every further insight in the internals of the Warcraft 3
real
datatype might improve existing resources, enable new possiblities and fix existing problems. Future points of interest could include (not exhaustive):- Find an efficient way to represent very small/big real values with the lack of scientific notation
- Implement a precision library for more convenient real handling
- Evaluate how the
LESS_THAN
etc. constants work, i.e. if they have the same accuracy as the real datatype or not. - Find out why the
NOT_EQUAL
constant doesn't work with unit state events. - Identify the accuracy of damage events
- And many more ...
Enclosed some concrete examples that are strange and need further investigation:
1. Dealing damage
The code
JASS:
call UnitDamageTarget(u, u, 0.0000000298023224, false, false, ATTACK_TYPE_CHAOS, DAMAGE_TYPE_UNIVERSAL, null)
should deal the specified damage, but
GetEventDamage
shows 0.158952544. It seems the smaller the literal gets, the bigger the actual damage is. Interesting would be by which regularity the damage increases and why.2. Literal values
It seems that
JASS:
local real r = 0.0000000000000000000000000000001
is the smallest postive literal that is not equal zero. However, this is not yet verified and it would be interesting why this is so since reals can represent smaller values than that.
So before this will be submitted as a tutorial I place it here in the lab to discuss this topic and include further findings and important facts about the Warcraft 3 real datatype.
Last edited: