Skip to content

Commit

Permalink
Remove most tables from wasm
Browse files Browse the repository at this point in the history
The WebAssembly build now depends only on win_contributions, rotations,
and reflections, which together take up only 19KB.  This shrinks
mid.wasm from 450,525 to 47,908 bytes.

This required making non-table-based versions of various functions.
Most such as pack/unpack were called few times, and sufficiently fast
versions were easy.  The hard function was halfsuper_wins, which went
from depending only the very large superwin_info table to the much
smaller win_contribution table, but got significantly slower as a
result.  Fortunately, I noticed that midengine was calling
halfsuper_wins more times than necessary, and better caching made the
final version faster than before this commit: 11.4 s to 10.9 s for
`unit.js mid`.
  • Loading branch information
girving committed Feb 14, 2020
1 parent 1674cd6 commit 645382c
Show file tree
Hide file tree
Showing 15 changed files with 238 additions and 84 deletions.
1 change: 1 addition & 0 deletions pentago/base/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ cc_tests(
"all_boards_test",
"pentago_test",
"super_test",
"slow_test",
],
deps = [
":base",
Expand Down
45 changes: 43 additions & 2 deletions pentago/base/board.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace pentago {
using std::min;
using std::swap;

#ifndef __wasm__
bool black_to_move(board_t board) {
check_board(board);
const side_t side0 = unpack(board,0),
Expand All @@ -23,14 +24,16 @@ bool black_to_move(board_t board) {
GEODE_ASSERT(count0==count1 || count1==count0+1);
return count0==count1;
}
#endif // !__wasm__

void check_board(board_t board) {
#define CHECK(q) \
if (!(quadrant(board,q) < 19683)) \
THROW(ValueError,"quadrant %d has invalid value %d",q,quadrant(board,q));
if (!(quadrant(board, q) < 19683)) \
THROW(ValueError, "quadrant %d has invalid value %d", q, quadrant(board, q));
CHECK(0) CHECK(1) CHECK(2) CHECK(3)
}

#ifndef __wasm__
static inline board_t pack(const Vector<Vector<quadrant_t,2>,4>& sides) {
return quadrants(pack(sides[0][0],sides[0][1]),
pack(sides[1][0],sides[1][1]),
Expand Down Expand Up @@ -65,6 +68,7 @@ board_t standardize(board_t board) {
}
return RawArray<board_t>(8,transformed).min();
}
#endif // !__wasm__

#ifndef __wasm__
Array<int,2> to_table(const board_t board) {
Expand Down Expand Up @@ -161,4 +165,41 @@ string str_board(board_t board) {
}
#endif // !__wasm__

// Slow versions for __wasm__ use

static const uint16_t threes[9] = {1, 3, 9, 27, 81, 243, 729, 2187, 6561};

board_t slow_pack(const side_t side0, const side_t side1) {
const uint64_t mask = 0x0001000100010001;
board_t board = 0;
for (const int i : range(9))
board += threes[i] * ((side0 >> i & mask) + 2 * (side1 >> i & mask));
return board;
}

Vector<side_t,2> slow_unpack(const board_t board) {
Vector<side_t,2> sides;
for (const int q : range(4)) {
const auto quad = quadrant(board, q);
for (const int i : range(9)) {
const auto t = quad / threes[i] % 3;
sides[0] |= uint64_t(t == 1) << (16*q + i);
sides[1] |= uint64_t(t == 2) << (16*q + i);
}
}
return sides;
}

int slow_count_stones(const board_t board) {
const auto [s0, s1] = slow_unpack(board);
return popcount(s0 | s1);
}

board_t slow_flip_board(const board_t board, const bool turn) {
auto sides = slow_unpack(board);
if (turn)
swap(sides[0], sides[1]);
return slow_pack(sides[0], sides[1]);
}

}
28 changes: 17 additions & 11 deletions pentago/base/board.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,18 @@ static inline uint64_t quadrants(quadrant_t q0, quadrant_t q1, quadrant_t q2, qu
return q0|(uint64_t)q1<<16|(uint64_t)q2<<32|(uint64_t)q3<<48;
}

#ifndef __wasm__
// Combine two side quadrants into a single double-sided quadrant
static inline quadrant_t pack(quadrant_t side0, quadrant_t side1) {
return pack_table[side0]+2*pack_table[side1];
return pack_table[side0] + 2*pack_table[side1];
}

// Pack two sides into a board
static inline board_t pack(side_t side0, side_t side1) {
return quadrants(pack(quadrant(side0,0),quadrant(side1,0)),
pack(quadrant(side0,1),quadrant(side1,1)),
pack(quadrant(side0,2),quadrant(side1,2)),
pack(quadrant(side0,3),quadrant(side1,3)));
return quadrants(pack(quadrant(side0, 0), quadrant(side1, 0)),
pack(quadrant(side0, 1), quadrant(side1, 1)),
pack(quadrant(side0, 2), quadrant(side1, 2)),
pack(quadrant(side0, 3), quadrant(side1, 3)));
}

// Extract one side from a double-sided quadrant
Expand All @@ -83,23 +84,19 @@ static inline Vector<side_t,2> unpack(board_t board) {

// Count the stones on a board
static inline int count_stones(board_t board) {
return popcount(unpack(board,0)|unpack(board,1));
return popcount(unpack(board, 0) | unpack(board, 1));
}

// Check whose turn it is (assuming black moved first)
bool black_to_move(board_t board);

// Throw ValueError if a board is invalid
void check_board(board_t board);

board_t standardize(board_t board);

// Maybe swap sides
static inline board_t flip_board(board_t board, bool turn = true) {
return pack(unpack(board,turn),unpack(board,1-turn));
return pack(unpack(board, turn), unpack(board, 1-turn));
}

#ifndef __wasm__
// Random board and side generation
side_t random_side(Random& random);
board_t random_board(Random& random);
Expand All @@ -114,4 +111,13 @@ Array<int,2> to_table(const board_t boards);
board_t from_table(RawArray<const int,2> tables);
#endif // !__wasm__

// Throw ValueError if a board is invalid
void check_board(board_t board);

// Slow versions for __wasm__ use
board_t slow_pack(const side_t side0, const side_t side1) __attribute__((const));
Vector<side_t,2> slow_unpack(const board_t board) __attribute__((const));
int slow_count_stones(const board_t board) __attribute__((const));
board_t slow_flip_board(const board_t board, const bool turn = true) __attribute__((const));

}
44 changes: 27 additions & 17 deletions pentago/base/precompute.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ using std::unordered_map;
struct unusable {};
#define REMEMBER(name, ...) const auto name = []() { __VA_ARGS__; return unusable(); }();

// type, name, c++ sizes, c++ initializer, js
vector<tuple<string,string,string,string,bool>> tables;
// type, name, c++ sizes, c++ initializer, flags
const int for_js = 1, for_wasm = 2;
vector<tuple<string,string,string,string,int>> tables;

template<class Data> void
cpp_init(ostream& out, const string& fmt, const Data& data, const int axis, const int offset) {
Expand All @@ -51,14 +52,14 @@ cpp_init(ostream& out, const string& fmt, const Data& data, const int axis, cons

// Remember a table we want to save
template<class Data> void
remember(const string& type, const string& name, const string& fmt, const Data& data, const bool js = false) {
remember(const string& type, const string& name, const string& fmt, const Data& data, const int flags = 0) {
const auto scalars = scalar_view(asarray(data));
string sizes;
for (const auto n : scalars.shape())
sizes += format("[%d]", n);
ostringstream init;
cpp_init(init, fmt, scalars.raw(), 0, 0);
tables.emplace_back(type, name, sizes, init.str(), js);
tables.emplace_back(type, name, sizes, init.str(), flags);
}

// Reformat an initializer for Javascript
Expand All @@ -83,17 +84,26 @@ void save(const string& h, const string& cc, const string& js) {
file_cc << "#include \"pentago/base/gen/tables.h\"\n";
file_cc << "namespace pentago {\n\n";

ofstream file_js(js.c_str());
file_js << note;

for (const auto& [type, name, sizes, init, js] : tables) {
file_h << format("extern const %s %s%s;\n", type, name, sizes);
file_cc << format("const %s %s%s = %s;\n", type, name, sizes, init);
if (js)
file_js << format("exports.%s = %s\n", name, js_init(init));
for (const int phase : {0, 1}) {
for (const auto& [type, name, sizes, init, flags] : tables) {
if (flags & for_wasm ? !phase : phase) {
file_h << format("extern const %s %s%s;\n", type, name, sizes);
file_cc << format("const %s %s%s = %s;\n", type, name, sizes, init);
}
}
const auto pre = phase ? "#endif // !defined(__wasm__)\n" : "#ifndef __wasm__\n";
file_h << pre;
file_cc << pre;
}
file_h << "\n}\n";
file_cc << "\n}\n";

// Javascript
ofstream file_js(js.c_str());
file_js << note;
for (const auto& [type, name, sizes, init, flags] : tables)
if (flags & for_js)
file_js << format("exports.%s = %s\n", name, js_init(init));
}

uint64_t ipow(uint64_t a, uint64_t b) {
Expand Down Expand Up @@ -244,7 +254,7 @@ const Array<const uint64_t,2> win_contributions = []() {
}
}
check(table, "4e5cf35e82fceecd464d73c3de35e6af4f75ee34");
remember("uint64_t", "win_contributions", "0x%xL", table);
remember("uint64_t", "win_contributions", "0x%xL", table, for_wasm);
return table;
}();

Expand All @@ -263,7 +273,7 @@ const Array<const Vector<uint16_t,2>> rotations = []() {
table[v] = vec(left, right);
}
check(table, "195f19d49311f82139a18ae681592de02b9954bc");
remember("uint16_t", "rotations", "0x%x", table);
remember("uint16_t", "rotations", "0x%x", table, for_wasm);
return table;
}();

Expand Down Expand Up @@ -390,7 +400,7 @@ const Array<const uint16_t> reflections = []() {
table[v] = r;
}
check(table, "2b23dc37f4bc1008eba3df0ee1b7815675b658bf");
remember("uint16_t", "reflections", "0x%x", table);
remember("uint16_t", "reflections", "0x%x", table, for_wasm);
return table;
}();

Expand Down Expand Up @@ -811,9 +821,9 @@ REMEMBER(rotation_minimal_quadrants,
check<uint64_t>(nest.flat, "8f48bb94ad675de569b07cca98a2e930b06b45ac");
check<uint64_t>(inverse, "339369694f78d4a197db8dc41a1f41300ba4f46c");
check<uint64_t>(moved, "dce212878aaebbcd995a8a0308335972bd1d5ef7");
remember("uint16_t", "rotation_minimal_quadrants_offsets", "%d", nest.offsets, true);
remember("uint16_t", "rotation_minimal_quadrants_offsets", "%d", nest.offsets, for_js);
remember("uint16_t", "rotation_minimal_quadrants_flat", "%d", nest.flat);
remember("uint16_t", "rotation_minimal_quadrants_inverse", "%d", inverse, true);
remember("uint16_t", "rotation_minimal_quadrants_inverse", "%d", inverse, for_js);
remember("uint16_t", "rotation_minimal_quadrants_reflect_moved", "%d", moved);
)

Expand Down
10 changes: 6 additions & 4 deletions pentago/base/score.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ static inline bool won(side_t side) {
* of winning occur between two boards, and 4 occur between 4, so a sum
* and a few bit twiddling checks are sufficient to test whether 5 in a
* row exists. See precompute for more details. */
uint64_t c = win_contributions[0][quadrant(side,0)]
+ win_contributions[1][quadrant(side,1)]
+ win_contributions[2][quadrant(side,2)]
+ win_contributions[3][quadrant(side,3)];
uint64_t c = win_contributions[0][quadrant(side, 0)]
+ win_contributions[1][quadrant(side, 1)]
+ win_contributions[2][quadrant(side, 2)]
+ win_contributions[3][quadrant(side, 3)];
return c&(c>>1)&0x55 // The first four ways of winning require contributions from three quadrants
|| c&(0xaaaaaaaaaaaaaaaa<<8); // The remaining 28 ways require contributions from only two
}

#ifndef __wasm__
// Determine if one side can win by rotating a quadrant
static inline bool rotated_won(side_t side) {
quadrant_t q0 = quadrant(side,0),
Expand Down Expand Up @@ -97,5 +98,6 @@ int arbitrarily_rotated_win_closeness(side_t black, side_t white) __attribute__(
// Warning: slow, and checks only one side
int rotated_status(board_t board);
int arbitrarily_rotated_status(board_t board);
#endif // !__wasm__

}
33 changes: 33 additions & 0 deletions pentago/base/slow_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#include "pentago/base/board.h"
#include "pentago/base/symmetry.h"
#include "gtest/gtest.h"
namespace pentago {
namespace {

TEST(slow, board) {
Random random(7);
for (int i = 0; i < 256; i++) {
const auto board = random_board(random);
const auto side0 = unpack(board, 0);
const auto side1 = unpack(board, 1);
const auto [slow0, slow1] = slow_unpack(board);
ASSERT_EQ(side0, slow0);
ASSERT_EQ(side1, slow1);
ASSERT_EQ(board, slow_pack(side0, side1));
ASSERT_EQ(count_stones(board), slow_count_stones(board));
for (const int turn : range(2))
ASSERT_EQ(flip_board(board, turn), slow_flip_board(board, turn));
}
}

TEST(slow, transform_board) {
Random random(7);
for (int i = 0; i < 256; i++) {
const auto s = random_symmetry(random);
const auto board = random_board(random);
ASSERT_EQ(transform_board(s, board), slow_transform_board(s, board));
}
}

} // namespace
} // namespace pentago
2 changes: 2 additions & 0 deletions pentago/base/superscore.cc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ struct superwin_info_t {
super_t horizontal, vertical, diagonal_lo, diagonal_hi, diagonal_assist;
};

#ifndef __wasm__
// We want to compute all possible rotations which give 5 in a row.
// To do this, we consider each pair or triple of quadrants which could give a win,
// and use the state space of the unused quadrants to store the various ways a win
Expand Down Expand Up @@ -96,6 +97,7 @@ super_t super_wins(side_t side) {
WAY(i1.diagonal_hi & i3.diagonal_assist & i2.diagonal_hi, OR0) // High diagonal from quadrant 1=(0,1) to 2=(1,0)
return wins;
}
#endif // !__wasm__

const Vector<int,4> single_rotations[8] = {
vec(1,0,0,0),vec(-1,0,0,0),vec(0,1,0,0),vec(0,-1,0,0),
Expand Down
9 changes: 7 additions & 2 deletions pentago/base/symmetry.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ static const uint8_t rotate_quadrants[4][4] = {{0,1,2,3},{1,3,0,2},{3,2,1,0},{2,

side_t transform_side(symmetry_t s, side_t side) {
// Decompose into quadrants
quadrant_t q[4] = {quadrant(side,0),quadrant(side,1),quadrant(side,2),quadrant(side,3)};
quadrant_t q[4] = {quadrant(side, 0), quadrant(side, 1), quadrant(side, 2), quadrant(side, 3)};
// Apply local rotations plus global rotations confined to each quadrant
for (int i=0;i<4;i++)
switch ((s.global+(s.local>>2*i))&3) {
Expand All @@ -43,6 +43,12 @@ side_t transform_side(symmetry_t s, side_t side) {
return quadrants(qr[0],qr[1],qr[2],qr[3]);
}

board_t slow_transform_board(symmetry_t s, board_t board) {
const auto [s0, s1] = slow_unpack(board);
return slow_pack(transform_side(s, s0), transform_side(s, s1));
}

#ifndef __wasm__
// I don't trust the compiler to figure out that all the branching is shared between sides,
// so this routine is two manually inlined copies of transform_side
board_t transform_board(symmetry_t s, board_t board) {
Expand Down Expand Up @@ -318,7 +324,6 @@ super_t super_meaningless(const board_t board, const uint64_t salt) {
return result;
}

#ifndef __wasm__
symmetry_t random_symmetry(Random& random) {
const int g = random.uniform<int>(0,symmetries.size());
return symmetry_t(g>>8,g&255);
Expand Down
9 changes: 8 additions & 1 deletion pentago/base/symmetry.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ struct symmetry_t {
return global^(~global>>2&(global&1))<<1;
}

#ifndef __wasm__
symmetry_t inverse() const {
uint8_t li = local_symmetry_t(local).inverse().local; // Invert local
uint8_t gi = invert_global(global); // Invert global
// Commute local through global
return symmetry_t(gi,commute_global_local_symmetries[gi][li]);
return symmetry_t(gi, commute_global_local_symmetries[gi][li]);
}
#endif // !__wasm__

explicit operator bool() const {
return global||local;
Expand All @@ -76,6 +78,7 @@ struct symmetry_t {
}
};

#ifndef __wasm__
// Group product
inline symmetry_t operator*(symmetry_t a, symmetry_t b) {
// We seek x = ag al bg bl = a.global a.local b.global b.local
Expand All @@ -94,12 +97,16 @@ inline symmetry_t operator*(local_symmetry_t a, symmetry_t b) {
const uint8_t l2 = commute_global_local_symmetries[b.global][a.local];
return symmetry_t(b.global,(local_symmetry_t(l2)*local_symmetry_t(b.local)).local);
}
#endif // !__wasm__

// A symmetry is a function s : X -> X, where X = Z_6^2 is the set of Pentago squares.
// A side_t is a subset A of X, and a board_t is two such subsets. These functions compute s(A).
// For example, transform_board(symmetry_t(1,0),board) rotates the whole board left by 90 degrees.
side_t transform_side(symmetry_t s, side_t side) __attribute__((const));
#ifndef __wasm__
board_t transform_board(symmetry_t s, board_t board) __attribute__((const));
#endif // !__wasm__
board_t slow_transform_board(symmetry_t s, board_t board) __attribute__((const));

// Let B be the set of boards, and A \subset B a subset of boards invariant to global transforms
// (b in A iff g(b) in A for g in D_4). Define the super operator f : B -> 2^L by
Expand Down
Loading

0 comments on commit 645382c

Please sign in to comment.