impl SkTDynamicHash with SkTHashTable
The only vaguely tricky thing to adapt from templating on T to T*. Don't think there's a need for the unit tests now right? Bug: skia:9703 Change-Id: Ib8fcebd9e0e35bea5ef23e6fd5962654a28d587b Reviewed-on: https://skia-review.googlesource.com/c/skia/+/277214 Auto-Submit: Mike Klein <mtklein@google.com> Commit-Queue: Herb Derby <herb@google.com> Reviewed-by: Herb Derby <herb@google.com>
This commit is contained in:
parent
aa0e45c232
commit
a30aeae875
@ -65,7 +65,6 @@ tests_sources = [
|
||||
"$_tests/DrawOpAtlasTest.cpp",
|
||||
"$_tests/DrawPathTest.cpp",
|
||||
"$_tests/DrawTextTest.cpp",
|
||||
"$_tests/DynamicHashTest.cpp",
|
||||
"$_tests/EmptyPathTest.cpp",
|
||||
"$_tests/EncodeTest.cpp",
|
||||
"$_tests/EncodedInfoTest.cpp",
|
||||
|
@ -8,9 +8,9 @@
|
||||
#ifndef SkTDynamicHash_DEFINED
|
||||
#define SkTDynamicHash_DEFINED
|
||||
|
||||
#include "include/core/SkMath.h"
|
||||
#include "include/core/SkTypes.h"
|
||||
#include "include/private/SkTemplates.h"
|
||||
// This is now a simple API wrapper around SkTHashTable<T*>;
|
||||
// please just use SkTHash{Map,Set,Table} directly for new code.
|
||||
#include "include/private/SkTHash.h"
|
||||
|
||||
// Traits requires:
|
||||
// static const Key& GetKey(const T&) { ... }
|
||||
@ -18,298 +18,39 @@
|
||||
// We'll look on T for these by default, or you can pass a custom Traits type.
|
||||
template <typename T,
|
||||
typename Key,
|
||||
typename Traits = T,
|
||||
int kGrowPercent = 75> // Larger -> more memory efficient, but slower.
|
||||
typename Traits = T>
|
||||
class SkTDynamicHash {
|
||||
public:
|
||||
SkTDynamicHash() : fCount(0), fDeleted(0), fCapacity(0), fArray(nullptr) {
|
||||
SkASSERT(this->validate());
|
||||
}
|
||||
SkTDynamicHash() {}
|
||||
|
||||
~SkTDynamicHash() {
|
||||
sk_free(fArray);
|
||||
}
|
||||
// It is not safe to call set() or remove() while iterating with either foreach().
|
||||
// If you mutate the entries be very careful not to change the Key.
|
||||
|
||||
// Call fn on every entry in the table. You may mutate the entries, but be very careful.
|
||||
template <typename Fn> // f(T*)
|
||||
void foreach(Fn&& fn) {
|
||||
for (Iter it(this); !it.done(); ++it) {
|
||||
fn(&*it);
|
||||
}
|
||||
fTable.foreach([&](T** entry) { fn(*entry); });
|
||||
}
|
||||
|
||||
// Call fn on every entry in the table. You may not mutate anything.
|
||||
template <typename Fn> // f(T) or f(const T&)
|
||||
void foreach(Fn&& fn) const {
|
||||
for (ConstIter it(this); !it.done(); ++it) {
|
||||
fn(*it);
|
||||
}
|
||||
fTable.foreach([&](T* entry) { fn(*entry); });
|
||||
}
|
||||
|
||||
int count() const { return fCount; }
|
||||
int count() const { return fTable.count(); }
|
||||
|
||||
// Return the entry with this key if we have it, otherwise nullptr.
|
||||
T* find(const Key& key) const {
|
||||
int index = this->firstIndex(key);
|
||||
for (int round = 0; round < fCapacity; round++) {
|
||||
SkASSERT(index >= 0 && index < fCapacity);
|
||||
T* candidate = fArray[index];
|
||||
if (Empty() == candidate) {
|
||||
return nullptr;
|
||||
}
|
||||
if (Deleted() != candidate && GetKey(*candidate) == key) {
|
||||
return candidate;
|
||||
}
|
||||
index = this->nextIndex(index, round);
|
||||
}
|
||||
SkASSERT(fCapacity == 0);
|
||||
return nullptr;
|
||||
}
|
||||
T* find(const Key& key) const { return fTable.findOrNull(key); }
|
||||
|
||||
// Add an entry with this key. We require that no entry with newEntry's key is already present.
|
||||
void add(T* newEntry) {
|
||||
SkASSERT(nullptr == this->find(GetKey(*newEntry)));
|
||||
this->maybeGrow();
|
||||
this->innerAdd(newEntry);
|
||||
SkASSERT(this->validate());
|
||||
}
|
||||
void add(T* entry) { fTable.set(entry); }
|
||||
void remove(const Key& key) { fTable.remove(key); }
|
||||
|
||||
// Remove the entry with this key. We require that an entry with this key is present.
|
||||
void remove(const Key& key) {
|
||||
SkASSERT(this->find(key));
|
||||
this->innerRemove(key);
|
||||
SkASSERT(this->validate());
|
||||
}
|
||||
|
||||
void rewind() {
|
||||
if (fArray) {
|
||||
sk_bzero(fArray, sizeof(T*)* fCapacity);
|
||||
}
|
||||
fCount = 0;
|
||||
fDeleted = 0;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
fCount = 0;
|
||||
fDeleted = 0;
|
||||
fCapacity = 0;
|
||||
sk_free(fArray);
|
||||
fArray = nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
// These methods are used by tests only.
|
||||
|
||||
int capacity() const { return fCapacity; }
|
||||
|
||||
// How many collisions do we go through before finding where this entry should be inserted?
|
||||
int countCollisions(const Key& key) const {
|
||||
int index = this->firstIndex(key);
|
||||
for (int round = 0; round < fCapacity; round++) {
|
||||
SkASSERT(index >= 0 && index < fCapacity);
|
||||
const T* candidate = fArray[index];
|
||||
if (Empty() == candidate || Deleted() == candidate || GetKey(*candidate) == key) {
|
||||
return round;
|
||||
}
|
||||
index = this->nextIndex(index, round);
|
||||
}
|
||||
SkASSERT(fCapacity == 0);
|
||||
return 0;
|
||||
}
|
||||
void rewind() { fTable.reset(); }
|
||||
void reset () { fTable.reset(); }
|
||||
|
||||
private:
|
||||
// We have two special values to indicate an empty or deleted entry.
|
||||
static T* Empty() { return reinterpret_cast<T*>(0); } // i.e. nullptr
|
||||
static T* Deleted() { return reinterpret_cast<T*>(1); } // Also an invalid pointer.
|
||||
|
||||
class Iter {
|
||||
public:
|
||||
explicit Iter(SkTDynamicHash* hash) : fHash(hash), fCurrentIndex(-1) {
|
||||
SkASSERT(hash);
|
||||
++(*this);
|
||||
}
|
||||
bool done() const {
|
||||
SkASSERT(fCurrentIndex <= fHash->fCapacity);
|
||||
return fCurrentIndex == fHash->fCapacity;
|
||||
}
|
||||
T& operator*() const {
|
||||
SkASSERT(!this->done());
|
||||
return *this->current();
|
||||
}
|
||||
void operator++() {
|
||||
do {
|
||||
fCurrentIndex++;
|
||||
} while (!this->done() && (this->current() == Empty() || this->current() == Deleted()));
|
||||
}
|
||||
|
||||
private:
|
||||
T* current() const { return fHash->fArray[fCurrentIndex]; }
|
||||
|
||||
SkTDynamicHash* fHash;
|
||||
int fCurrentIndex;
|
||||
struct AdaptedTraits {
|
||||
static const Key& GetKey(T* entry) { return Traits::GetKey(*entry); }
|
||||
static uint32_t Hash(const Key& key) { return Traits::Hash(key); }
|
||||
};
|
||||
|
||||
|
||||
class ConstIter {
|
||||
public:
|
||||
explicit ConstIter(const SkTDynamicHash* hash) : fHash(hash), fCurrentIndex(-1) {
|
||||
SkASSERT(hash);
|
||||
++(*this);
|
||||
}
|
||||
bool done() const {
|
||||
SkASSERT(fCurrentIndex <= fHash->fCapacity);
|
||||
return fCurrentIndex == fHash->fCapacity;
|
||||
}
|
||||
const T& operator*() const {
|
||||
SkASSERT(!this->done());
|
||||
return *this->current();
|
||||
}
|
||||
void operator++() {
|
||||
do {
|
||||
fCurrentIndex++;
|
||||
} while (!this->done() && (this->current() == Empty() || this->current() == Deleted()));
|
||||
}
|
||||
|
||||
private:
|
||||
const T* current() const { return fHash->fArray[fCurrentIndex]; }
|
||||
|
||||
const SkTDynamicHash* fHash;
|
||||
int fCurrentIndex;
|
||||
};
|
||||
|
||||
bool validate() const {
|
||||
#define SKTDYNAMICHASH_CHECK(x) SkASSERT(x); if (!(x)) return false
|
||||
static const int kLarge = 50; // Arbitrary, tweak to suit your patience.
|
||||
|
||||
// O(1) checks, always done.
|
||||
// Is capacity sane?
|
||||
SKTDYNAMICHASH_CHECK(SkIsPow2(fCapacity));
|
||||
|
||||
// O(N) checks, skipped when very large.
|
||||
if (fCount < kLarge * kLarge) {
|
||||
// Are fCount and fDeleted correct, and are all elements findable?
|
||||
int count = 0, deleted = 0;
|
||||
for (int i = 0; i < fCapacity; i++) {
|
||||
if (Deleted() == fArray[i]) {
|
||||
deleted++;
|
||||
} else if (Empty() != fArray[i]) {
|
||||
count++;
|
||||
SKTDYNAMICHASH_CHECK(this->find(GetKey(*fArray[i])));
|
||||
}
|
||||
}
|
||||
SKTDYNAMICHASH_CHECK(count == fCount);
|
||||
SKTDYNAMICHASH_CHECK(deleted == fDeleted);
|
||||
}
|
||||
|
||||
// O(N^2) checks, skipped when large.
|
||||
if (fCount < kLarge) {
|
||||
// Are all entries unique?
|
||||
for (int i = 0; i < fCapacity; i++) {
|
||||
if (Empty() == fArray[i] || Deleted() == fArray[i]) {
|
||||
continue;
|
||||
}
|
||||
for (int j = i+1; j < fCapacity; j++) {
|
||||
if (Empty() == fArray[j] || Deleted() == fArray[j]) {
|
||||
continue;
|
||||
}
|
||||
SKTDYNAMICHASH_CHECK(fArray[i] != fArray[j]);
|
||||
SKTDYNAMICHASH_CHECK(!(GetKey(*fArray[i]) == GetKey(*fArray[j])));
|
||||
}
|
||||
}
|
||||
}
|
||||
#undef SKTDYNAMICHASH_CHECK
|
||||
return true;
|
||||
}
|
||||
|
||||
void innerAdd(T* newEntry) {
|
||||
const Key& key = GetKey(*newEntry);
|
||||
int index = this->firstIndex(key);
|
||||
for (int round = 0; round < fCapacity; round++) {
|
||||
SkASSERT(index >= 0 && index < fCapacity);
|
||||
const T* candidate = fArray[index];
|
||||
if (Empty() == candidate || Deleted() == candidate) {
|
||||
if (Deleted() == candidate) {
|
||||
fDeleted--;
|
||||
}
|
||||
fCount++;
|
||||
fArray[index] = newEntry;
|
||||
return;
|
||||
}
|
||||
index = this->nextIndex(index, round);
|
||||
}
|
||||
SkASSERT(fCapacity == 0);
|
||||
}
|
||||
|
||||
void innerRemove(const Key& key) {
|
||||
const int firstIndex = this->firstIndex(key);
|
||||
int index = firstIndex;
|
||||
for (int round = 0; round < fCapacity; round++) {
|
||||
SkASSERT(index >= 0 && index < fCapacity);
|
||||
const T* candidate = fArray[index];
|
||||
if (Deleted() != candidate && GetKey(*candidate) == key) {
|
||||
fDeleted++;
|
||||
fCount--;
|
||||
fArray[index] = Deleted();
|
||||
return;
|
||||
}
|
||||
index = this->nextIndex(index, round);
|
||||
}
|
||||
SkASSERT(fCapacity == 0);
|
||||
}
|
||||
|
||||
void maybeGrow() {
|
||||
if (100 * (fCount + fDeleted + 1) > fCapacity * kGrowPercent) {
|
||||
auto newCapacity = fCapacity > 0 ? fCapacity : 4;
|
||||
|
||||
// Only grow the storage when most non-empty entries are
|
||||
// in active use. Otherwise, just purge the tombstones.
|
||||
if (fCount > fDeleted) {
|
||||
newCapacity *= 2;
|
||||
}
|
||||
SkASSERT(newCapacity > fCount + 1);
|
||||
|
||||
this->resize(newCapacity);
|
||||
}
|
||||
}
|
||||
|
||||
void resize(int newCapacity) {
|
||||
SkDEBUGCODE(int oldCount = fCount;)
|
||||
int oldCapacity = fCapacity;
|
||||
SkAutoTMalloc<T*> oldArray(fArray);
|
||||
|
||||
fCount = fDeleted = 0;
|
||||
fCapacity = newCapacity;
|
||||
fArray = (T**)sk_calloc_throw(sizeof(T*) * fCapacity);
|
||||
|
||||
for (int i = 0; i < oldCapacity; i++) {
|
||||
T* entry = oldArray[i];
|
||||
if (Empty() != entry && Deleted() != entry) {
|
||||
this->innerAdd(entry);
|
||||
}
|
||||
}
|
||||
SkASSERT(oldCount == fCount);
|
||||
}
|
||||
|
||||
// fCapacity is always a power of 2, so this masks the correct low bits to index into our hash.
|
||||
uint32_t hashMask() const { return fCapacity - 1; }
|
||||
|
||||
int firstIndex(const Key& key) const {
|
||||
return Hash(key) & this->hashMask();
|
||||
}
|
||||
|
||||
// Given index at round N, what is the index to check at N+1? round should start at 0.
|
||||
int nextIndex(int index, int round) const {
|
||||
// This will search a power-of-two array fully without repeating an index.
|
||||
return (index + round + 1) & this->hashMask();
|
||||
}
|
||||
|
||||
static const Key& GetKey(const T& t) { return Traits::GetKey(t); }
|
||||
static uint32_t Hash(const Key& key) { return Traits::Hash(key); }
|
||||
|
||||
int fCount; // Number of non Empty(), non Deleted() entries in fArray.
|
||||
int fDeleted; // Number of Deleted() entries in fArray.
|
||||
int fCapacity; // Number of entries in fArray. Always a power of 2.
|
||||
T** fArray;
|
||||
SkTHashTable<T*, Key, AdaptedTraits> fTable;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -1,210 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license that can be
|
||||
* found in the LICENSE file.
|
||||
*/
|
||||
|
||||
#include "include/core/SkTypes.h"
|
||||
#include "src/core/SkTDynamicHash.h"
|
||||
#include "tests/Test.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
|
||||
struct Entry {
|
||||
int key;
|
||||
double value;
|
||||
|
||||
static const int& GetKey(const Entry& entry) { return entry.key; }
|
||||
static uint32_t Hash(const int& key) { return key; }
|
||||
};
|
||||
|
||||
|
||||
class Hash : public SkTDynamicHash<Entry, int> {
|
||||
public:
|
||||
Hash() : INHERITED() {}
|
||||
|
||||
// Promote protected methods to public for this test.
|
||||
int capacity() const { return this->INHERITED::capacity(); }
|
||||
int countCollisions(const int& key) const { return this->INHERITED::countCollisions(key); }
|
||||
|
||||
private:
|
||||
typedef SkTDynamicHash<Entry, int> INHERITED;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#define ASSERT(x) REPORTER_ASSERT(reporter, x)
|
||||
|
||||
DEF_TEST(DynamicHash_growth, reporter) {
|
||||
Entry a = { 1, 2.0 };
|
||||
Entry b = { 2, 3.0 };
|
||||
Entry c = { 3, 4.0 };
|
||||
Entry d = { 4, 5.0 };
|
||||
Entry e = { 5, 6.0 };
|
||||
|
||||
Hash hash;
|
||||
ASSERT(hash.capacity() == 0);
|
||||
|
||||
hash.add(&a);
|
||||
ASSERT(hash.capacity() == 4);
|
||||
|
||||
hash.add(&b);
|
||||
ASSERT(hash.capacity() == 4);
|
||||
|
||||
hash.add(&c);
|
||||
ASSERT(hash.capacity() == 4);
|
||||
|
||||
hash.add(&d);
|
||||
ASSERT(hash.capacity() == 8);
|
||||
|
||||
hash.add(&e);
|
||||
ASSERT(hash.capacity() == 8);
|
||||
|
||||
ASSERT(hash.count() == 5);
|
||||
}
|
||||
|
||||
DEF_TEST(DynamicHash_growth_bounded, reporter) {
|
||||
Entry a = { 1, 2.0 };
|
||||
Entry b = { 2, 3.0 };
|
||||
Entry c = { 3, 4.0 };
|
||||
Entry d = { 4, 5.0 };
|
||||
Entry e = { 5, 6.0 };
|
||||
|
||||
Hash hash;
|
||||
ASSERT(hash.capacity() == 0);
|
||||
|
||||
hash.add(&a);
|
||||
ASSERT(hash.capacity() == 4);
|
||||
|
||||
hash.remove(a.key);
|
||||
hash.add(&b);
|
||||
ASSERT(hash.capacity() == 4);
|
||||
|
||||
hash.remove(b.key);
|
||||
hash.add(&c);
|
||||
ASSERT(hash.capacity() == 4);
|
||||
|
||||
hash.remove(c.key);
|
||||
hash.add(&d);
|
||||
ASSERT(hash.capacity() == 4);
|
||||
|
||||
hash.remove(d.key);
|
||||
hash.add(&e);
|
||||
ASSERT(hash.capacity() == 4);
|
||||
|
||||
ASSERT(hash.count() == 1);
|
||||
}
|
||||
|
||||
DEF_TEST(DynamicHash_add, reporter) {
|
||||
Hash hash;
|
||||
Entry a = { 1, 2.0 };
|
||||
Entry b = { 2, 3.0 };
|
||||
|
||||
ASSERT(hash.count() == 0);
|
||||
hash.add(&a);
|
||||
ASSERT(hash.count() == 1);
|
||||
hash.add(&b);
|
||||
ASSERT(hash.count() == 2);
|
||||
}
|
||||
|
||||
DEF_TEST(DynamicHash_lookup, reporter) {
|
||||
Hash hash;
|
||||
|
||||
// These collide.
|
||||
Entry a = { 1, 2.0 };
|
||||
Entry b = { 5, 3.0 };
|
||||
|
||||
// Before we insert anything, nothing can collide.
|
||||
ASSERT(hash.countCollisions(1) == 0);
|
||||
ASSERT(hash.countCollisions(5) == 0);
|
||||
ASSERT(hash.countCollisions(9) == 0);
|
||||
|
||||
// First is easy.
|
||||
hash.add(&a);
|
||||
ASSERT(hash.countCollisions(1) == 0);
|
||||
ASSERT(hash.countCollisions(5) == 1);
|
||||
ASSERT(hash.countCollisions(9) == 1);
|
||||
|
||||
// Second is one step away.
|
||||
hash.add(&b);
|
||||
ASSERT(hash.countCollisions(1) == 0);
|
||||
ASSERT(hash.countCollisions(5) == 1);
|
||||
ASSERT(hash.countCollisions(9) == 2);
|
||||
|
||||
// We can find our data right?
|
||||
ASSERT(hash.find(1) != nullptr);
|
||||
ASSERT(hash.find(1)->value == 2.0);
|
||||
ASSERT(hash.find(5) != nullptr);
|
||||
ASSERT(hash.find(5)->value == 3.0);
|
||||
|
||||
// These aren't in the hash.
|
||||
ASSERT(hash.find(2) == nullptr);
|
||||
ASSERT(hash.find(9) == nullptr);
|
||||
}
|
||||
|
||||
DEF_TEST(DynamicHash_remove, reporter) {
|
||||
Hash hash;
|
||||
|
||||
// These collide.
|
||||
Entry a = { 1, 2.0 };
|
||||
Entry b = { 5, 3.0 };
|
||||
Entry c = { 9, 4.0 };
|
||||
|
||||
hash.add(&a);
|
||||
hash.add(&b);
|
||||
hash.remove(1);
|
||||
// a should be marked deleted, and b should still be findable.
|
||||
|
||||
ASSERT(hash.find(1) == nullptr);
|
||||
ASSERT(hash.find(5) != nullptr);
|
||||
ASSERT(hash.find(5)->value == 3.0);
|
||||
|
||||
// This will go in the same slot as 'a' did before.
|
||||
ASSERT(hash.countCollisions(9) == 0);
|
||||
hash.add(&c);
|
||||
ASSERT(hash.find(9) != nullptr);
|
||||
ASSERT(hash.find(9)->value == 4.0);
|
||||
ASSERT(hash.find(5) != nullptr);
|
||||
ASSERT(hash.find(5)->value == 3.0);
|
||||
}
|
||||
|
||||
static void TestResetOrRewind(skiatest::Reporter* reporter, bool testReset) {
|
||||
Hash hash;
|
||||
Entry a = { 1, 2.0 };
|
||||
Entry b = { 2, 3.0 };
|
||||
|
||||
ASSERT(hash.capacity() == 0);
|
||||
hash.add(&a);
|
||||
hash.add(&b);
|
||||
ASSERT(hash.count() == 2);
|
||||
ASSERT(hash.capacity() == 4);
|
||||
|
||||
if (testReset) {
|
||||
hash.reset();
|
||||
ASSERT(hash.capacity() == 0);
|
||||
} else {
|
||||
hash.rewind();
|
||||
ASSERT(hash.capacity() == 4);
|
||||
}
|
||||
ASSERT(hash.count() == 0);
|
||||
|
||||
// make sure things still work
|
||||
hash.add(&a);
|
||||
hash.add(&b);
|
||||
ASSERT(hash.count() == 2);
|
||||
ASSERT(hash.capacity() == 4);
|
||||
|
||||
ASSERT(hash.find(1) != nullptr);
|
||||
ASSERT(hash.find(2) != nullptr);
|
||||
}
|
||||
|
||||
DEF_TEST(DynamicHash_reset, reporter) {
|
||||
TestResetOrRewind(reporter, true);
|
||||
}
|
||||
|
||||
DEF_TEST(DynamicHash_rewind, reporter) {
|
||||
TestResetOrRewind(reporter, false);
|
||||
}
|
Loading…
Reference in New Issue
Block a user