BeyondCritics wrote:Here is my final proposal:
Code: Select all
#include <climits>
#include <cstdint>
// SHRT_MIN < v <= SHRT_MAX must hold for all valid Values v.
enum Value : int {};
enum Score : int {};
/// mg, eg must be valid Values
Score make_score(int mg, int eg) {
static const int val_bits = 16;
return Score((eg << val_bits) + mg);
}
/// Extract the end game value from a score
Value eg_value(Score s) {
static const int val_bits = 16;
static const int min_val = SHRT_MIN;
return Value((s - min_val) >> val_bits);
}
/// Extract the middle game value from a score
Value mg_value(Score s) {
return Value(int16_t(s));
}
Enter this into
http://gcc.godbolt.org/ and you will see, that it always elicits seemingly optimal assembly.
It relies on 2^s complement and arithmetic right shift though.
Getting optimal assembly is not so difficult. The much-criticised current SF approach already does that.
The question is how to do it reliably without undefined behavior and, preferably, without relying on any implementation-defined behavior.
Your make_score() has undefined behavior: left-shift of negative ints. Fixing this by casting eg to unsigned will give implementation-defined behavior: casting of unsigned int to int of values which don't all fit in int because they can be >= 0x80000000.
Your eg_value() relies on implementation-defined behavior: right-shift of negative ints.
Your mg_value() also relies on implementation-defined behavior: cast to int16_t of values outside the range of int16_t.
What I proposed earlier does not suffer from such problems (I believe), but icc seems unable to optimise it fully. gcc and clang have no problems with it.
Code: Select all
#include <cstdint>
enum Value : int { VALUE_ZERO };
enum Score : uint32_t { SCORE_ZERO };
Score make_score(int mg, int eg) {
return Score((uint32_t(eg) << 16) + mg);
}
inline int16_t cast_to_int16_t(uint16_t v) {
return v < 0x8000 ? v : v - 0x10000;
}
Value eg_value(Score s) {
return Value(cast_to_int16_t((s + 0x8000) >> 16));
}
Value mg_value(Score s) {
return Value(cast_to_int16_t(s));
}
If we decide that full optimisation on all relevant compilers is more important than making sure it works on Xbox 360, then I propose:
Code: Select all
#include <cstdint>
enum Value : int { VALUE_ZERO };
enum Score : uint32_t { SCORE_ZERO };
Score make_score(int mg, int eg) {
return Score((uint32_t(eg) << 16) + mg);
}
Value eg_value(Score s) {
return Value(int16_t((s + 0x8000) >> 16));
}
Value mg_value(Score s) {
return Value(int16_t(s));
}
Now both eg_value() and mg_value() rely on the compiler casting out-of-range values to the unique int16_t value that is equal to the original value mod 2^16 and that's it - if I don't overlook anything.
Alternatively, we could implement
SF's approach correctly (icc 13.0.1 messes it up, but icc 16 and 17 get it):
Code: Select all
#include <cstdint>
#include <cstring>
enum Value : int { VALUE_ZERO };
enum Score : uint32_t { SCORE_ZERO };
Score make_score(int mg, int eg) {
return Score((uint32_t(eg) << 16) + mg);
}
Value eg_value(Score score) {
int16_t s;
uint16_t u = (score + 0x8000) >> 16;
memcpy(&s, &u, sizeof(int16_t));
return Value(s);
}
Value mg_value(Score score) {
int16_t s;
uint16_t u = score;
memcpy(&s, &u, sizeof(int16_t));
return Value(s);
}
This relies on bit representation of int16_t and uint16_t being "the same". I think that can go wrong only in theory.