Optimize DefUseManager allocations (#4709)

* Optimize DefUseManager allocations

Saves around 30-35% of compilation time.

For inst->use_ids, use a pool linked list instead of allocating vectors for every instruction.  For inst->uses, use a "PooledLinkedList"' -- a linked list that has shared storage for all nodes.  Neither re-use nodes, instead we do a bulk compaction operation when too much memory is being wasted (tuneable).

Includes separate PooledLinkedList templated datastructure, a very special case construct, but split out to make the code a little easier to understand.
This commit is contained in:
pd-valve 2022-02-15 16:17:30 -08:00 коммит произвёл GitHub
Родитель a123632ed9
Коммит d18d0d92e5
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 942 добавлений и 139 удалений

Просмотреть файл

@ -455,6 +455,7 @@ static_library("spvtools") {
"source/util/make_unique.h",
"source/util/parse_number.cpp",
"source/util/parse_number.h",
"source/util/pooled_linked_list.h",
"source/util/small_vector.h",
"source/util/string_utils.cpp",
"source/util/string_utils.h",

Просмотреть файл

@ -228,6 +228,7 @@ set(SPIRV_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/util/hex_float.h
${CMAKE_CURRENT_SOURCE_DIR}/util/make_unique.h
${CMAKE_CURRENT_SOURCE_DIR}/util/parse_number.h
${CMAKE_CURRENT_SOURCE_DIR}/util/pooled_linked_list.h
${CMAKE_CURRENT_SOURCE_DIR}/util/small_vector.h
${CMAKE_CURRENT_SOURCE_DIR}/util/string_utils.h
${CMAKE_CURRENT_SOURCE_DIR}/util/timer.h

Просмотреть файл

@ -13,11 +13,23 @@
// limitations under the License.
#include "source/opt/def_use_manager.h"
#include "source/util/make_unique.h"
namespace spvtools {
namespace opt {
namespace analysis {
// Don't compact before we have a reasonable number of ids allocated (~32kb).
static const size_t kCompactThresholdMinTotalIds = (8 * 1024);
// Compact when fewer than this fraction of the storage is used (should be 2^n
// for performance).
static const size_t kCompactThresholdFractionFreeIds = 8;
DefUseManager::DefUseManager() {
use_pool_ = MakeUnique<UseListPool>();
used_id_pool_ = MakeUnique<UsedIdListPool>();
}
void DefUseManager::AnalyzeInstDef(Instruction* inst) {
const uint32_t def_id = inst->result_id();
if (def_id != 0) {
@ -34,15 +46,15 @@ void DefUseManager::AnalyzeInstDef(Instruction* inst) {
}
void DefUseManager::AnalyzeInstUse(Instruction* inst) {
// It might have existed before.
EraseUseRecordsOfOperandIds(inst);
// Create entry for the given instruction. Note that the instruction may
// not have any in-operands. In such cases, we still need a entry for those
// instructions so this manager knows it has seen the instruction later.
auto* used_ids = &inst_to_used_ids_[inst];
if (used_ids->size()) {
EraseUseRecordsOfOperandIds(inst);
used_ids = &inst_to_used_ids_[inst];
}
used_ids->clear(); // It might have existed before.
UsedIdList& used_ids =
inst_to_used_id_.insert({inst, UsedIdList(used_id_pool_.get())})
.first->second;
for (uint32_t i = 0; i < inst->NumOperands(); ++i) {
switch (inst->GetOperand(i).type) {
@ -54,8 +66,17 @@ void DefUseManager::AnalyzeInstUse(Instruction* inst) {
uint32_t use_id = inst->GetSingleWordOperand(i);
Instruction* def = GetDef(use_id);
assert(def && "Definition is not registered.");
id_to_users_.insert(UserEntry{def, inst});
used_ids->push_back(use_id);
// Add to inst's use records
used_ids.push_back(use_id);
// Add to the users, taking care to avoid adding duplicates. We know
// the duplicate for this instruction will always be at the tail.
UseList& list = inst_to_users_.insert({def, UseList(use_pool_.get())})
.first->second;
if (list.empty() || list.back() != inst) {
list.push_back(inst);
}
} break;
default:
break;
@ -94,23 +115,6 @@ const Instruction* DefUseManager::GetDef(uint32_t id) const {
return iter->second;
}
DefUseManager::IdToUsersMap::const_iterator DefUseManager::UsersBegin(
const Instruction* def) const {
return id_to_users_.lower_bound(
UserEntry{const_cast<Instruction*>(def), nullptr});
}
bool DefUseManager::UsersNotEnd(const IdToUsersMap::const_iterator& iter,
const IdToUsersMap::const_iterator& cached_end,
const Instruction* inst) const {
return (iter != cached_end && iter->def == inst);
}
bool DefUseManager::UsersNotEnd(const IdToUsersMap::const_iterator& iter,
const Instruction* inst) const {
return UsersNotEnd(iter, id_to_users_.end(), inst);
}
bool DefUseManager::WhileEachUser(
const Instruction* def, const std::function<bool(Instruction*)>& f) const {
// Ensure that |def| has been registered.
@ -118,9 +122,11 @@ bool DefUseManager::WhileEachUser(
"Definition is not registered.");
if (!def->HasResultId()) return true;
auto end = id_to_users_.end();
for (auto iter = UsersBegin(def); UsersNotEnd(iter, end, def); ++iter) {
if (!f(iter->user)) return false;
auto iter = inst_to_users_.find(def);
if (iter != inst_to_users_.end()) {
for (Instruction* user : iter->second) {
if (!f(user)) return false;
}
}
return true;
}
@ -151,14 +157,15 @@ bool DefUseManager::WhileEachUse(
"Definition is not registered.");
if (!def->HasResultId()) return true;
auto end = id_to_users_.end();
for (auto iter = UsersBegin(def); UsersNotEnd(iter, end, def); ++iter) {
Instruction* user = iter->user;
for (uint32_t idx = 0; idx != user->NumOperands(); ++idx) {
const Operand& op = user->GetOperand(idx);
if (op.type != SPV_OPERAND_TYPE_RESULT_ID && spvIsIdType(op.type)) {
if (def->result_id() == op.words[0]) {
if (!f(user, idx)) return false;
auto iter = inst_to_users_.find(def);
if (iter != inst_to_users_.end()) {
for (Instruction* user : iter->second) {
for (uint32_t idx = 0; idx != user->NumOperands(); ++idx) {
const Operand& op = user->GetOperand(idx);
if (op.type != SPV_OPERAND_TYPE_RESULT_ID && spvIsIdType(op.type)) {
if (def->result_id() == op.words[0]) {
if (!f(user, idx)) return false;
}
}
}
}
@ -230,17 +237,18 @@ void DefUseManager::AnalyzeDefUse(Module* module) {
}
void DefUseManager::ClearInst(Instruction* inst) {
auto iter = inst_to_used_ids_.find(inst);
if (iter != inst_to_used_ids_.end()) {
if (inst_to_used_id_.find(inst) != inst_to_used_id_.end()) {
EraseUseRecordsOfOperandIds(inst);
if (inst->result_id() != 0) {
// Remove all uses of this inst.
auto users_begin = UsersBegin(inst);
auto end = id_to_users_.end();
auto new_end = users_begin;
for (; UsersNotEnd(new_end, end, inst); ++new_end) {
uint32_t const result_id = inst->result_id();
if (result_id != 0) {
// For each using instruction, remove result_id from their used ids.
auto iter = inst_to_users_.find(inst);
if (iter != inst_to_users_.end()) {
for (Instruction* use : iter->second) {
inst_to_used_id_.at(use).remove_first(result_id);
}
inst_to_users_.erase(iter);
}
id_to_users_.erase(users_begin, new_end);
id_to_def_.erase(inst->result_id());
}
}
@ -249,16 +257,48 @@ void DefUseManager::ClearInst(Instruction* inst) {
void DefUseManager::EraseUseRecordsOfOperandIds(const Instruction* inst) {
// Go through all ids used by this instruction, remove this instruction's
// uses of them.
auto iter = inst_to_used_ids_.find(inst);
if (iter != inst_to_used_ids_.end()) {
for (auto use_id : iter->second) {
id_to_users_.erase(
UserEntry{GetDef(use_id), const_cast<Instruction*>(inst)});
auto iter = inst_to_used_id_.find(inst);
if (iter != inst_to_used_id_.end()) {
const UsedIdList& used_ids = iter->second;
for (uint32_t def_id : used_ids) {
auto def_iter = inst_to_users_.find(GetDef(def_id));
if (def_iter != inst_to_users_.end()) {
def_iter->second.remove_first(const_cast<Instruction*>(inst));
}
}
inst_to_used_id_.erase(inst);
// If we're using only a fraction of the space in used_ids_, compact storage
// to prevent memory usage from being unbounded.
if (used_id_pool_->total_nodes() > kCompactThresholdMinTotalIds &&
used_id_pool_->used_nodes() <
used_id_pool_->total_nodes() / kCompactThresholdFractionFreeIds) {
CompactStorage();
}
inst_to_used_ids_.erase(iter);
}
}
void DefUseManager::CompactStorage() {
CompactUseRecords();
CompactUsedIds();
}
void DefUseManager::CompactUseRecords() {
std::unique_ptr<UseListPool> new_pool = MakeUnique<UseListPool>();
for (auto& iter : inst_to_users_) {
iter.second.move_nodes(new_pool.get());
}
use_pool_ = std::move(new_pool);
}
void DefUseManager::CompactUsedIds() {
std::unique_ptr<UsedIdListPool> new_pool = MakeUnique<UsedIdListPool>();
for (auto& iter : inst_to_used_id_) {
iter.second.move_nodes(new_pool.get());
}
used_id_pool_ = std::move(new_pool);
}
bool CompareAndPrintDifferences(const DefUseManager& lhs,
const DefUseManager& rhs) {
bool same = true;
@ -277,34 +317,52 @@ bool CompareAndPrintDifferences(const DefUseManager& lhs,
same = false;
}
if (lhs.id_to_users_ != rhs.id_to_users_) {
for (auto p : lhs.id_to_users_) {
if (rhs.id_to_users_.count(p) == 0) {
printf("Diff in id_to_users: missing value in rhs\n");
}
for (const auto& l : lhs.inst_to_used_id_) {
std::set<uint32_t> ul, ur;
lhs.ForEachUse(l.first,
[&ul](Instruction*, uint32_t id) { ul.insert(id); });
rhs.ForEachUse(l.first,
[&ur](Instruction*, uint32_t id) { ur.insert(id); });
if (ul.size() != ur.size()) {
printf(
"Diff in inst_to_used_id_: different number of used ids (%zu != %zu)",
ul.size(), ur.size());
same = false;
} else if (ul != ur) {
printf("Diff in inst_to_used_id_: different used ids\n");
same = false;
}
for (auto p : rhs.id_to_users_) {
if (lhs.id_to_users_.count(p) == 0) {
printf("Diff in id_to_users: missing value in lhs\n");
}
}
for (const auto& r : rhs.inst_to_used_id_) {
auto iter_l = lhs.inst_to_used_id_.find(r.first);
if (r.second.empty() &&
!(iter_l == lhs.inst_to_used_id_.end() || iter_l->second.empty())) {
printf("Diff in inst_to_used_id_: unexpected instr in rhs\n");
same = false;
}
same = false;
}
if (lhs.inst_to_used_ids_ != rhs.inst_to_used_ids_) {
for (auto p : lhs.inst_to_used_ids_) {
if (rhs.inst_to_used_ids_.count(p.first) == 0) {
printf("Diff in inst_to_used_ids: missing value in rhs\n");
}
for (const auto& l : lhs.inst_to_users_) {
std::set<Instruction*> ul, ur;
lhs.ForEachUser(l.first, [&ul](Instruction* use) { ul.insert(use); });
rhs.ForEachUser(l.first, [&ur](Instruction* use) { ur.insert(use); });
if (ul.size() != ur.size()) {
printf("Diff in inst_to_users_: different number of users (%zu != %zu)",
ul.size(), ur.size());
same = false;
} else if (ul != ur) {
printf("Diff in inst_to_users_: different users\n");
same = false;
}
}
for (const auto& r : rhs.inst_to_users_) {
auto iter_l = lhs.inst_to_users_.find(r.first);
if (r.second.empty() &&
!(iter_l == lhs.inst_to_users_.end() || iter_l->second.empty())) {
printf("Diff in inst_to_users_: unexpected instr in rhs\n");
same = false;
}
for (auto p : rhs.inst_to_used_ids_) {
if (lhs.inst_to_used_ids_.count(p.first) == 0) {
printf("Diff in inst_to_used_ids: missing value in lhs\n");
}
}
same = false;
}
return same;
}

Просмотреть файл

@ -21,6 +21,7 @@
#include "source/opt/instruction.h"
#include "source/opt/module.h"
#include "source/util/pooled_linked_list.h"
#include "spirv-tools/libspirv.hpp"
namespace spvtools {
@ -49,50 +50,6 @@ inline bool operator<(const Use& lhs, const Use& rhs) {
return lhs.operand_index < rhs.operand_index;
}
// Definition should never be null. User can be null, however, such an entry
// should be used only for searching (e.g. all users of a particular definition)
// and never stored in a container.
struct UserEntry {
Instruction* def;
Instruction* user;
};
inline bool operator==(const UserEntry& lhs, const UserEntry& rhs) {
return lhs.def == rhs.def && lhs.user == rhs.user;
}
// Orders UserEntry for use in associative containers (i.e. less than ordering).
//
// The definition of an UserEntry is treated as the major key and the users as
// the minor key so that all the users of a particular definition are
// consecutive in a container.
//
// A null user always compares less than a real user. This is done to provide
// easy values to search for the beginning of the users of a particular
// definition (i.e. using {def, nullptr}).
struct UserEntryLess {
bool operator()(const UserEntry& lhs, const UserEntry& rhs) const {
// If lhs.def and rhs.def are both null, fall through to checking the
// second entries.
if (!lhs.def && rhs.def) return true;
if (lhs.def && !rhs.def) return false;
// If neither definition is null, then compare unique ids.
if (lhs.def && rhs.def) {
if (lhs.def->unique_id() < rhs.def->unique_id()) return true;
if (rhs.def->unique_id() < lhs.def->unique_id()) return false;
}
// Return false on equality.
if (!lhs.user && !rhs.user) return false;
if (!lhs.user) return true;
if (!rhs.user) return false;
// If neither user is null then compare unique ids.
return lhs.user->unique_id() < rhs.user->unique_id();
}
};
// A class for analyzing and managing defs and uses in an Module.
class DefUseManager {
public:
@ -102,7 +59,7 @@ class DefUseManager {
// will be communicated to the outside via the given message |consumer|. This
// instance only keeps a reference to the |consumer|, so the |consumer| should
// outlive this instance.
DefUseManager(Module* module) { AnalyzeDefUse(module); }
DefUseManager(Module* module) : DefUseManager() { AnalyzeDefUse(module); }
DefUseManager(const DefUseManager&) = delete;
DefUseManager(DefUseManager&&) = delete;
@ -214,35 +171,36 @@ class DefUseManager {
// uses.
void UpdateDefUse(Instruction* inst);
// Compacts any internal storage to save memory.
void CompactStorage();
private:
using IdToUsersMap = std::set<UserEntry, UserEntryLess>;
using InstToUsedIdsMap =
std::unordered_map<const Instruction*, std::vector<uint32_t>>;
using UseList = spvtools::utils::PooledLinkedList<Instruction*>;
using UseListPool = spvtools::utils::PooledLinkedListNodes<Instruction*>;
// Stores linked lists of Instructions using a def.
using InstToUsersMap = std::unordered_map<const Instruction*, UseList>;
// Returns the first location that {|def|, nullptr} could be inserted into the
// users map without violating ordering.
IdToUsersMap::const_iterator UsersBegin(const Instruction* def) const;
using UsedIdList = spvtools::utils::PooledLinkedList<uint32_t>;
using UsedIdListPool = spvtools::utils::PooledLinkedListNodes<uint32_t>;
// Stores mapping from instruction to their UsedIdRange.
using InstToUsedIdMap = std::unordered_map<const Instruction*, UsedIdList>;
// Returns true if |iter| has not reached the end of |def|'s users.
//
// In the first version |iter| is compared against the end of the map for
// validity before other checks. In the second version, |iter| is compared
// against |cached_end| for validity before other checks. This allows caching
// the map's end which is a performance improvement on some platforms.
bool UsersNotEnd(const IdToUsersMap::const_iterator& iter,
const Instruction* def) const;
bool UsersNotEnd(const IdToUsersMap::const_iterator& iter,
const IdToUsersMap::const_iterator& cached_end,
const Instruction* def) const;
DefUseManager();
// Analyzes the defs and uses in the given |module| and populates data
// structures in this class. Does nothing if |module| is nullptr.
void AnalyzeDefUse(Module* module);
IdToDefMap id_to_def_; // Mapping from ids to their definitions
IdToUsersMap id_to_users_; // Mapping from ids to their users
// Mapping from instructions to the ids used in the instruction.
InstToUsedIdsMap inst_to_used_ids_;
// Removes unused entries in used_records_ and used_ids_.
void CompactUseRecords();
void CompactUsedIds();
IdToDefMap id_to_def_; // Mapping from ids to their definitions
InstToUsersMap inst_to_users_; // Map from def to uses.
std::unique_ptr<UseListPool> use_pool_;
std::unique_ptr<UsedIdListPool> used_id_pool_;
InstToUsedIdMap inst_to_used_id_; // Map from instruction to used ids.
};
} // namespace analysis

Просмотреть файл

@ -0,0 +1,236 @@
// Copyright (c) 2021 The Khronos Group Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef SOURCE_UTIL_POOLED_LINKED_LIST_H_
#define SOURCE_UTIL_POOLED_LINKED_LIST_H_
#include <cstdint>
#include <vector>
namespace spvtools {
namespace utils {
// Shared storage of nodes for PooledLinkedList.
template <typename T>
class PooledLinkedListNodes {
public:
struct Node {
Node(T e, int32_t n = -1) : element(e), next(n) {}
T element = {};
int32_t next = -1;
};
PooledLinkedListNodes() = default;
PooledLinkedListNodes(const PooledLinkedListNodes&) = delete;
PooledLinkedListNodes& operator=(const PooledLinkedListNodes&) = delete;
PooledLinkedListNodes(PooledLinkedListNodes&& that) {
*this = std::move(that);
}
PooledLinkedListNodes& operator=(PooledLinkedListNodes&& that) {
vec_ = std::move(that.vec_);
free_nodes_ = that.free_nodes_;
return *this;
}
size_t total_nodes() { return vec_.size(); }
size_t free_nodes() { return free_nodes_; }
size_t used_nodes() { return total_nodes() - free_nodes(); }
private:
template <typename ListT>
friend class PooledLinkedList;
Node& at(int32_t index) { return vec_[index]; }
const Node& at(int32_t index) const { return vec_[index]; }
int32_t insert(T element) {
int32_t index = int32_t(vec_.size());
vec_.emplace_back(element);
return index;
}
std::vector<Node> vec_;
size_t free_nodes_ = 0;
};
// Implements a linked-list where list nodes come from a shared pool. This is
// meant to be used in scenarios where it is desirable to avoid many small
// allocations.
//
// Instead of pointers, the list uses indices to allow the underlying storage
// to be modified without needing to modify the list. When removing elements
// from the list, nodes are not deleted or recycled: to reclaim unused space,
// perform a sequence of |move_nodes| operations into a temporary pool, which
// then is moved into the old pool.
//
// This does *not* attempt to implement a full stl-compatible interface.
template <typename T>
class PooledLinkedList {
public:
using NodePool = PooledLinkedListNodes<T>;
using Node = typename NodePool::Node;
PooledLinkedList() = delete;
PooledLinkedList(NodePool* nodes) : nodes_(nodes) {}
// Shared iterator implementation (for iterator and const_iterator).
template <typename ElementT, typename PoolT>
class iterator_base {
public:
iterator_base(const iterator_base& i)
: nodes_(i.nodes_), index_(i.index_) {}
iterator_base& operator++() {
index_ = nodes_->at(index_).next;
return *this;
}
iterator_base& operator=(const iterator_base& i) {
nodes_ = i.nodes_;
index_ = i.index_;
return *this;
}
ElementT& operator*() const { return nodes_->at(index_).element; }
ElementT* operator->() const { return &nodes_->at(index_).element; }
friend inline bool operator==(const iterator_base& lhs,
const iterator_base& rhs) {
return lhs.nodes_ == rhs.nodes_ && lhs.index_ == rhs.index_;
}
friend inline bool operator!=(const iterator_base& lhs,
const iterator_base& rhs) {
return lhs.nodes_ != rhs.nodes_ || lhs.index_ != rhs.index_;
}
// Define standard iterator types needs so this class can be
// used with <algorithms>.
using iterator_category = std::forward_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = ElementT;
using pointer = ElementT*;
using const_pointer = const ElementT*;
using reference = ElementT&;
using const_reference = const ElementT&;
using size_type = size_t;
private:
friend PooledLinkedList;
iterator_base(PoolT* pool, int32_t index) : nodes_(pool), index_(index) {}
PoolT* nodes_;
int32_t index_ = -1;
};
using iterator = iterator_base<T, std::vector<Node>>;
using const_iterator = iterator_base<const T, const std::vector<Node>>;
bool empty() const { return head_ == -1; }
T& front() { return nodes_->at(head_).element; }
T& back() { return nodes_->at(tail_).element; }
const T& front() const { return nodes_->at(head_).element; }
const T& back() const { return nodes_->at(tail_).element; }
iterator begin() { return iterator(&nodes_->vec_, head_); }
iterator end() { return iterator(&nodes_->vec_, -1); }
const_iterator begin() const { return const_iterator(&nodes_->vec_, head_); }
const_iterator end() const { return const_iterator(&nodes_->vec_, -1); }
// Inserts |element| at the back of the list.
void push_back(T element) {
int32_t new_tail = nodes_->insert(element);
if (head_ == -1) {
head_ = new_tail;
tail_ = new_tail;
} else {
nodes_->at(tail_).next = new_tail;
tail_ = new_tail;
}
}
// Removes the first occurrence of |element| from the list.
// Returns if |element| was removed.
bool remove_first(T element) {
int32_t* prev_next = &head_;
for (int32_t prev_index = -1, index = head_; index != -1; /**/) {
auto& node = nodes_->at(index);
if (node.element == element) {
// Snip from of the list, optionally fixing up tail pointer.
if (tail_ == index) {
assert(node.next == -1);
tail_ = prev_index;
}
*prev_next = node.next;
nodes_->free_nodes_++;
return true;
} else {
prev_next = &node.next;
}
prev_index = index;
index = node.next;
}
return false;
}
// Returns the PooledLinkedListNodes that owns this list's nodes.
NodePool* pool() { return nodes_; }
// Moves the nodes in this list into |new_pool|, providing a way to compact
// storage and reclaim unused space.
//
// Upon completing a sequence of |move_nodes| calls, you must ensure you
// retain ownership of the new storage your lists point to. Example usage:
//
// unique_ptr<NodePool> new_pool = ...;
// for (PooledLinkedList& list : lists) {
// list.move_to(new_pool);
// }
// my_pool_ = std::move(new_pool);
void move_nodes(NodePool* new_pool) {
// Be sure to construct the list in the same order, instead of simply
// doing a sequence of push_backs.
int32_t prev_entry = -1;
int32_t nodes_freed = 0;
for (int32_t index = head_; index != -1; nodes_freed++) {
const auto& node = nodes_->at(index);
int32_t this_entry = new_pool->insert(node.element);
index = node.next;
if (prev_entry == -1) {
head_ = this_entry;
} else {
new_pool->at(prev_entry).next = this_entry;
}
prev_entry = this_entry;
}
tail_ = prev_entry;
// Update our old pool's free count, now we're a member of the new pool.
nodes_->free_nodes_ += nodes_freed;
nodes_ = new_pool;
}
private:
NodePool* nodes_;
int32_t head_ = -1;
int32_t tail_ = -1;
};
} // namespace utils
} // namespace spvtools
#endif // SOURCE_UTIL_POOLED_LINKED_LIST_H_

Просмотреть файл

@ -950,6 +950,369 @@ INSTANTIATE_TEST_SUITE_P(
);
// clang-format on
// We re-use the same replace usecases, we need to similarly exercise the
// DefUseManager by replacing instructions and uses.
using CompactIdempotence = ::testing::TestWithParam<ReplaceUseCase>;
TEST_P(CompactIdempotence, Case) {
const auto& tc = GetParam();
// Build module.
const std::vector<const char*> text = {tc.before};
std::unique_ptr<IRContext> context =
BuildModule(SPV_ENV_UNIVERSAL_1_1, nullptr, JoinAllInsts(text),
SPV_TEXT_TO_BINARY_OPTION_PRESERVE_NUMERIC_IDS);
ASSERT_NE(nullptr, context);
// Force a re-build of def-use manager.
context->InvalidateAnalyses(IRContext::Analysis::kAnalysisDefUse);
(void)context->get_def_use_mgr();
// Do the substitution.
for (const auto& candidate : tc.candidates) {
context->ReplaceAllUsesWith(candidate.first, candidate.second);
}
// Ensure new/uncompacted managers produce the same result
EXPECT_TRUE(CompareAndPrintDifferences(*context->get_def_use_mgr(),
DefUseManager(context->module())));
EXPECT_EQ(tc.after, DisassembleModule(context->module()));
CheckDef(tc.du, context->get_def_use_mgr()->id_to_defs());
CheckUse(tc.du, context->get_def_use_mgr(), context->module()->IdBound());
// Compare again after compacting the defuse manager's storage
context->get_def_use_mgr()->CompactStorage();
CheckDef(tc.du, context->get_def_use_mgr()->id_to_defs());
CheckUse(tc.du, context->get_def_use_mgr(), context->module()->IdBound());
EXPECT_TRUE(CompareAndPrintDifferences(*context->get_def_use_mgr(),
DefUseManager(context->module())));
}
// clang-format off
INSTANTIATE_TEST_SUITE_P(
TestCase, CompactIdempotence,
::testing::ValuesIn(std::vector<ReplaceUseCase>{
{ // no use, no replace request
"", {}, "", {},
},
{ // replace one use
"%1 = OpTypeBool "
"%2 = OpTypeVector %1 3 "
"%3 = OpTypeInt 32 0 ",
{{1, 3}},
"%1 = OpTypeBool\n"
"%2 = OpTypeVector %3 3\n"
"%3 = OpTypeInt 32 0",
{
{ // defs
{1, "%1 = OpTypeBool"},
{2, "%2 = OpTypeVector %3 3"},
{3, "%3 = OpTypeInt 32 0"},
},
{ // uses
{3, {"%2 = OpTypeVector %3 3"}},
},
},
},
{ // replace and then replace back
"%1 = OpTypeBool "
"%2 = OpTypeVector %1 3 "
"%3 = OpTypeInt 32 0",
{{1, 3}, {3, 1}},
"%1 = OpTypeBool\n"
"%2 = OpTypeVector %1 3\n"
"%3 = OpTypeInt 32 0",
{
{ // defs
{1, "%1 = OpTypeBool"},
{2, "%2 = OpTypeVector %1 3"},
{3, "%3 = OpTypeInt 32 0"},
},
{ // uses
{1, {"%2 = OpTypeVector %1 3"}},
},
},
},
{ // replace with the same id
"%1 = OpTypeBool "
"%2 = OpTypeVector %1 3",
{{1, 1}, {2, 2}, {3, 3}},
"%1 = OpTypeBool\n"
"%2 = OpTypeVector %1 3",
{
{ // defs
{1, "%1 = OpTypeBool"},
{2, "%2 = OpTypeVector %1 3"},
},
{ // uses
{1, {"%2 = OpTypeVector %1 3"}},
},
},
},
{ // replace in sequence
"%1 = OpTypeBool "
"%2 = OpTypeVector %1 3 "
"%3 = OpTypeInt 32 0 "
"%4 = OpTypeInt 32 1 ",
{{1, 3}, {3, 4}},
"%1 = OpTypeBool\n"
"%2 = OpTypeVector %4 3\n"
"%3 = OpTypeInt 32 0\n"
"%4 = OpTypeInt 32 1",
{
{ // defs
{1, "%1 = OpTypeBool"},
{2, "%2 = OpTypeVector %4 3"},
{3, "%3 = OpTypeInt 32 0"},
{4, "%4 = OpTypeInt 32 1"},
},
{ // uses
{4, {"%2 = OpTypeVector %4 3"}},
},
},
},
{ // replace multiple uses
"%1 = OpTypeBool "
"%2 = OpTypeVector %1 2 "
"%3 = OpTypeVector %1 3 "
"%4 = OpTypeVector %1 4 "
"%5 = OpTypeMatrix %2 2 "
"%6 = OpTypeMatrix %3 3 "
"%7 = OpTypeMatrix %4 4 "
"%8 = OpTypeInt 32 0 "
"%9 = OpTypeInt 32 1 "
"%10 = OpTypeInt 64 0",
{{1, 8}, {2, 9}, {4, 10}},
"%1 = OpTypeBool\n"
"%2 = OpTypeVector %8 2\n"
"%3 = OpTypeVector %8 3\n"
"%4 = OpTypeVector %8 4\n"
"%5 = OpTypeMatrix %9 2\n"
"%6 = OpTypeMatrix %3 3\n"
"%7 = OpTypeMatrix %10 4\n"
"%8 = OpTypeInt 32 0\n"
"%9 = OpTypeInt 32 1\n"
"%10 = OpTypeInt 64 0",
{
{ // defs
{1, "%1 = OpTypeBool"},
{2, "%2 = OpTypeVector %8 2"},
{3, "%3 = OpTypeVector %8 3"},
{4, "%4 = OpTypeVector %8 4"},
{5, "%5 = OpTypeMatrix %9 2"},
{6, "%6 = OpTypeMatrix %3 3"},
{7, "%7 = OpTypeMatrix %10 4"},
{8, "%8 = OpTypeInt 32 0"},
{9, "%9 = OpTypeInt 32 1"},
{10, "%10 = OpTypeInt 64 0"},
},
{ // uses
{8,
{
"%2 = OpTypeVector %8 2",
"%3 = OpTypeVector %8 3",
"%4 = OpTypeVector %8 4",
}
},
{9, {"%5 = OpTypeMatrix %9 2"}},
{3, {"%6 = OpTypeMatrix %3 3"}},
{10, {"%7 = OpTypeMatrix %10 4"}},
},
},
},
{ // OpPhi.
kOpPhiTestFunction,
// replace one id used by OpPhi, replace one id generated by OpPhi
{{9, 13}, {11, 9}},
"%1 = OpTypeVoid\n"
"%6 = OpTypeInt 32 0\n"
"%10 = OpTypeFloat 32\n"
"%16 = OpTypeBool\n"
"%3 = OpTypeFunction %1\n"
"%8 = OpConstant %6 0\n"
"%18 = OpConstant %6 1\n"
"%12 = OpConstant %10 1\n"
"%2 = OpFunction %1 None %3\n"
"%4 = OpLabel\n"
"OpBranch %5\n"
"%5 = OpLabel\n"
"%7 = OpPhi %6 %8 %4 %13 %5\n" // %9 -> %13
"%11 = OpPhi %10 %12 %4 %13 %5\n"
"%9 = OpIAdd %6 %7 %8\n"
"%13 = OpFAdd %10 %9 %12\n" // %11 -> %9
"%17 = OpSLessThan %16 %7 %18\n"
"OpLoopMerge %19 %5 None\n"
"OpBranchConditional %17 %5 %19\n"
"%19 = OpLabel\n"
"OpReturn\n"
"OpFunctionEnd",
{
{ // defs.
{1, "%1 = OpTypeVoid"},
{2, "%2 = OpFunction %1 None %3"},
{3, "%3 = OpTypeFunction %1"},
{4, "%4 = OpLabel"},
{5, "%5 = OpLabel"},
{6, "%6 = OpTypeInt 32 0"},
{7, "%7 = OpPhi %6 %8 %4 %13 %5"},
{8, "%8 = OpConstant %6 0"},
{9, "%9 = OpIAdd %6 %7 %8"},
{10, "%10 = OpTypeFloat 32"},
{11, "%11 = OpPhi %10 %12 %4 %13 %5"},
{12, "%12 = OpConstant %10 1.0"},
{13, "%13 = OpFAdd %10 %9 %12"},
{16, "%16 = OpTypeBool"},
{17, "%17 = OpSLessThan %16 %7 %18"},
{18, "%18 = OpConstant %6 1"},
{19, "%19 = OpLabel"},
},
{ // uses
{1,
{
"%2 = OpFunction %1 None %3",
"%3 = OpTypeFunction %1",
}
},
{3, {"%2 = OpFunction %1 None %3"}},
{4,
{
"%7 = OpPhi %6 %8 %4 %13 %5",
"%11 = OpPhi %10 %12 %4 %13 %5",
}
},
{5,
{
"OpBranch %5",
"%7 = OpPhi %6 %8 %4 %13 %5",
"%11 = OpPhi %10 %12 %4 %13 %5",
"OpLoopMerge %19 %5 None",
"OpBranchConditional %17 %5 %19",
}
},
{6,
{
// Can't properly check constants
// "%8 = OpConstant %6 0",
// "%18 = OpConstant %6 1",
"%7 = OpPhi %6 %8 %4 %13 %5",
"%9 = OpIAdd %6 %7 %8"
}
},
{7,
{
"%9 = OpIAdd %6 %7 %8",
"%17 = OpSLessThan %16 %7 %18",
}
},
{8,
{
"%7 = OpPhi %6 %8 %4 %13 %5",
"%9 = OpIAdd %6 %7 %8",
}
},
{9, {"%13 = OpFAdd %10 %9 %12"}}, // uses of %9 changed from %7 to %13
{10,
{
"%11 = OpPhi %10 %12 %4 %13 %5",
// "%12 = OpConstant %10 1",
"%13 = OpFAdd %10 %9 %12"
}
},
// no more uses of %11
{12,
{
"%11 = OpPhi %10 %12 %4 %13 %5",
"%13 = OpFAdd %10 %9 %12"
}
},
{13, {
"%7 = OpPhi %6 %8 %4 %13 %5",
"%11 = OpPhi %10 %12 %4 %13 %5",
}
},
{16, {"%17 = OpSLessThan %16 %7 %18"}},
{17, {"OpBranchConditional %17 %5 %19"}},
{18, {"%17 = OpSLessThan %16 %7 %18"}},
{19,
{
"OpLoopMerge %19 %5 None",
"OpBranchConditional %17 %5 %19",
}
},
},
},
},
{ // OpPhi defining and referencing the same id.
"%1 = OpTypeBool "
"%3 = OpTypeFunction %1 "
"%2 = OpConstantTrue %1 "
"%4 = OpFunction %3 None %1 "
"%6 = OpLabel "
" OpBranch %7 "
"%7 = OpLabel "
"%8 = OpPhi %1 %8 %7 %2 %6 " // both defines and uses %8
" OpBranch %7 "
" OpFunctionEnd",
{{8, 2}},
"%1 = OpTypeBool\n"
"%3 = OpTypeFunction %1\n"
"%2 = OpConstantTrue %1\n"
"%4 = OpFunction %3 None %1\n"
"%6 = OpLabel\n"
"OpBranch %7\n"
"%7 = OpLabel\n"
"%8 = OpPhi %1 %2 %7 %2 %6\n" // use of %8 changed to %2
"OpBranch %7\n"
"OpFunctionEnd",
{
{ // defs
{1, "%1 = OpTypeBool"},
{2, "%2 = OpConstantTrue %1"},
{3, "%3 = OpTypeFunction %1"},
{4, "%4 = OpFunction %3 None %1"},
{6, "%6 = OpLabel"},
{7, "%7 = OpLabel"},
{8, "%8 = OpPhi %1 %2 %7 %2 %6"},
},
{ // uses
{1,
{
"%2 = OpConstantTrue %1",
"%3 = OpTypeFunction %1",
"%4 = OpFunction %3 None %1",
"%8 = OpPhi %1 %2 %7 %2 %6",
}
},
{2,
{
// Only checking users
"%8 = OpPhi %1 %2 %7 %2 %6",
}
},
{3, {"%4 = OpFunction %3 None %1"}},
{6, {"%8 = OpPhi %1 %2 %7 %2 %6"}},
{7,
{
"OpBranch %7",
"%8 = OpPhi %1 %2 %7 %2 %6",
"OpBranch %7",
}
},
// {8, {"%8 = OpPhi %1 %8 %7 %2 %6"}},
},
},
},
})
);
// clang-format on
struct KillDefCase {
const char* before;
std::vector<uint32_t> ids_to_kill;

Просмотреть файл

@ -17,6 +17,7 @@ add_spvtools_unittest(TARGET utils
bit_vector_test.cpp
bitutils_test.cpp
hash_combine_test.cpp
pooled_linked_list_test.cpp
small_vector_test.cpp
LIBS SPIRV-Tools-opt
)

Просмотреть файл

@ -0,0 +1,185 @@
// Copyright (c) 2021 The Khronos Group Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <algorithm>
#include <list>
#include <utility>
#include <vector>
#include "gmock/gmock.h"
#include "source/util/pooled_linked_list.h"
namespace spvtools {
namespace utils {
namespace {
using PooledLinkedListTest = ::testing::Test;
template <typename T>
static std::vector<T> ToVector(const PooledLinkedList<T>& list) {
std::vector<T> vec;
for (auto it = list.begin(); it != list.end(); ++it) {
vec.push_back(*it);
}
return vec;
}
template <typename T>
static void AppendVector(PooledLinkedList<T>& list, const std::vector<T>& vec) {
for (const T& t : vec) {
list.push_back(t);
}
}
TEST(PooledLinkedListTest, Empty) {
PooledLinkedListNodes<uint32_t> pool;
PooledLinkedList<uint32_t> ll(&pool);
EXPECT_TRUE(ll.empty());
ll.push_back(1u);
EXPECT_TRUE(!ll.empty());
}
TEST(PooledLinkedListTest, Iterator) {
PooledLinkedListNodes<uint32_t> pool;
PooledLinkedList<uint32_t> ll(&pool);
EXPECT_EQ(ll.begin(), ll.end());
ll.push_back(1);
EXPECT_NE(ll.begin(), ll.end());
auto it = ll.begin();
EXPECT_EQ(*it, 1);
++it;
EXPECT_EQ(it, ll.end());
}
TEST(PooledLinkedListTest, Iterator_algorithms) {
PooledLinkedListNodes<uint32_t> pool;
PooledLinkedList<uint32_t> ll(&pool);
AppendVector(ll, {3, 2, 0, 1});
EXPECT_EQ(std::distance(ll.begin(), ll.end()), 4);
EXPECT_EQ(*std::min_element(ll.begin(), ll.end()), 0);
EXPECT_EQ(*std::max_element(ll.begin(), ll.end()), 3);
}
TEST(PooledLinkedListTest, FrontBack) {
PooledLinkedListNodes<uint32_t> pool;
PooledLinkedList<uint32_t> ll(&pool);
ll.push_back(1);
EXPECT_EQ(ll.front(), 1);
EXPECT_EQ(ll.back(), 1);
ll.push_back(2);
EXPECT_EQ(ll.front(), 1);
EXPECT_EQ(ll.back(), 2);
}
TEST(PooledLinkedListTest, PushBack) {
const std::vector<uint32_t> vec = {1, 2, 3, 4, 5, 6};
PooledLinkedListNodes<uint32_t> pool;
PooledLinkedList<uint32_t> ll(&pool);
AppendVector(ll, vec);
EXPECT_EQ(vec, ToVector(ll));
}
TEST(PooledLinkedListTest, RemoveFirst) {
const std::vector<uint32_t> vec = {1, 2, 3, 4, 5, 6};
PooledLinkedListNodes<uint32_t> pool;
PooledLinkedList<uint32_t> ll(&pool);
EXPECT_FALSE(ll.remove_first(0));
AppendVector(ll, vec);
EXPECT_FALSE(ll.remove_first(0));
std::vector<uint32_t> tmp = vec;
while (!tmp.empty()) {
size_t mid = tmp.size() / 2;
uint32_t elt = tmp[mid];
tmp.erase(tmp.begin() + mid);
EXPECT_TRUE(ll.remove_first(elt));
EXPECT_FALSE(ll.remove_first(elt));
EXPECT_EQ(tmp, ToVector(ll));
}
EXPECT_TRUE(ll.empty());
}
TEST(PooledLinkedListTest, RemoveFirst_Duplicates) {
const std::vector<uint32_t> vec = {3, 1, 2, 3, 3, 3, 3, 4, 3, 5, 3, 6, 3};
PooledLinkedListNodes<uint32_t> pool;
PooledLinkedList<uint32_t> ll(&pool);
AppendVector(ll, vec);
std::vector<uint32_t> tmp = vec;
while (!tmp.empty()) {
size_t mid = tmp.size() / 2;
uint32_t elt = tmp[mid];
tmp.erase(std::find(tmp.begin(), tmp.end(), elt));
EXPECT_TRUE(ll.remove_first(elt));
EXPECT_EQ(tmp, ToVector(ll));
}
EXPECT_TRUE(ll.empty());
}
TEST(PooledLinkedList, MoveTo) {
const std::vector<uint32_t> vec = {1, 2, 3, 4, 5, 6};
PooledLinkedListNodes<uint32_t> pool;
PooledLinkedList<uint32_t> ll1(&pool);
PooledLinkedList<uint32_t> ll2(&pool);
PooledLinkedList<uint32_t> ll3(&pool);
AppendVector(ll1, vec);
AppendVector(ll2, vec);
AppendVector(ll3, vec);
EXPECT_EQ(pool.total_nodes(), vec.size() * 3);
EXPECT_EQ(pool.total_nodes(), vec.size() * 3);
EXPECT_EQ(pool.free_nodes(), 0);
// Move two lists to the new pool
PooledLinkedListNodes<uint32_t> pool_new;
ll1.move_nodes(&pool_new);
ll2.move_nodes(&pool_new);
// Moved nodes should belong to new pool
EXPECT_EQ(ll1.pool(), &pool_new);
EXPECT_EQ(ll2.pool(), &pool_new);
// Old pool should be smaller & have free nodes.
EXPECT_EQ(pool.used_nodes(), vec.size());
EXPECT_EQ(pool.free_nodes(), vec.size() * 2);
// New pool should be sized exactly and no free nodes.
EXPECT_EQ(pool_new.total_nodes(), vec.size() * 2);
EXPECT_EQ(pool_new.used_nodes(), vec.size() * 2);
EXPECT_EQ(pool_new.free_nodes(), 0);
// All lists should be preserved
EXPECT_EQ(ToVector(ll1), vec);
EXPECT_EQ(ToVector(ll2), vec);
EXPECT_EQ(ToVector(ll3), vec);
}
} // namespace
} // namespace utils
} // namespace spvtools