Add move constructor and assignment operator to TypedValue

Summary:
`heaptrack` shows a miniscule decrease in memory usage during query
execution.

Running the below query on the TEDTalk database 100 times gives the
following results:

- number of allocations: from 642647 to 642589
- bytes allocated in total: from 48.79 MiB to 48.78 MiB

```
MATCH (t:Tag)<-[:HasTag]-(n:Talk)
RETURN t.name AS Tag, COUNT(n) AS TalksCount
ORDER BY TalksCount DESC, Tag LIMIT 20;
```

Regarding `TypedValue`'s destructor: we're allowed to manually destruct the
union memebers that we constructed using placement-new. However, it is undefined
behavior to call the destructor after an object's lifetime has ended. Calling
`TypedValue`'s own destructor within its assignment operator counts as ending
its lifetime, which means that the next call to its destructor will invoke
undefined behavior.

See the C++ Draft N4140, clauses 12.4/15 and 3.8/1.3.

Reviewers: teon.banek, mtomic

Reviewed By: teon.banek

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D1630
This commit is contained in:
Lovro Lugovic 2018-10-09 11:36:21 +02:00
parent 4e5fe37dd6
commit 5bdb04a693
5 changed files with 261 additions and 93 deletions

View File

@ -74,7 +74,7 @@ ProduceRpcServer::OngoingProduce::PullOneFromCursor() {
if (cursor_->Pull(frame_, context_)) {
results.reserve(pull_symbols_.size());
for (const auto &symbol : pull_symbols_) {
results.emplace_back(std::move(frame_[symbol]));
results.push_back(frame_[symbol]);
}
} else {
cursor_state_ = PullState::CURSOR_EXHAUSTED;

View File

@ -3165,18 +3165,10 @@ class CartesianCursor : public Cursor {
}
bool Pull(Frame &frame, Context &context) override {
auto copy_frame = [&frame]() {
std::vector<TypedValue> result;
for (auto &elem : frame.elems()) {
result.emplace_back(std::move(elem));
}
return result;
};
if (!cartesian_pull_initialized_) {
// Pull all left_op frames.
while (left_op_cursor_->Pull(frame, context)) {
left_op_frames_.emplace_back(copy_frame());
left_op_frames_.emplace_back(frame.elems());
}
// We're setting the iterator to 'end' here so it pulls the right
@ -3201,7 +3193,7 @@ class CartesianCursor : public Cursor {
// Advance right_op_cursor_.
if (!right_op_cursor_->Pull(frame, context)) return false;
right_op_frame_ = copy_frame();
right_op_frame_ = frame.elems();
left_op_frames_it_ = left_op_frames_.begin();
} else {
// Make sure right_op_cursor last pulled results are on frame.

View File

@ -4,6 +4,7 @@
#include <cmath>
#include <iostream>
#include <memory>
#include <utility>
#include "glog/logging.h"
@ -50,6 +51,51 @@ TypedValue::TypedValue(const PropertyValue &value) {
LOG(FATAL) << "Unsupported type";
}
TypedValue::TypedValue(PropertyValue &&value) {
switch (value.type()) {
case PropertyValue::Type::Null:
type_ = Type::Null;
break;
case PropertyValue::Type::Bool:
type_ = Type::Bool;
bool_v = value.Value<bool>();
break;
case PropertyValue::Type::Int:
type_ = Type::Int;
int_v = value.Value<int64_t>();
break;
case PropertyValue::Type::Double:
type_ = Type::Double;
double_v = value.Value<double>();
break;
case PropertyValue::Type::String:
type_ = Type::String;
// TODO: std::move() when PropertyValue is fixed
new (&string_v) std::string(value.Value<std::string>());
break;
case PropertyValue::Type::List: {
// TODO: std::move() when PropertyValue is fixed
type_ = Type::List;
auto vec = value.Value<std::vector<PropertyValue>>();
new (&list_v)
std::vector<TypedValue>(std::make_move_iterator(vec.begin()),
std::make_move_iterator(vec.end()));
break;
}
case PropertyValue::Type::Map: {
// TODO: std::move() when PropertyValue is fixed
type_ = Type::Map;
auto map = value.Value<std::map<std::string, PropertyValue>>();
new (&map_v) std::map<std::string, TypedValue>(
std::make_move_iterator(map.begin()),
std::make_move_iterator(map.end()));
break;
}
}
value = PropertyValue::Null;
}
TypedValue::TypedValue(const TypedValue &other) : type_(other.type_) {
switch (other.type_) {
case TypedValue::Type::Null:
@ -85,6 +131,42 @@ TypedValue::TypedValue(const TypedValue &other) : type_(other.type_) {
LOG(FATAL) << "Unsupported TypedValue::Type";
}
TypedValue::TypedValue(TypedValue &&other) : type_(other.type_) {
switch (other.type_) {
case TypedValue::Type::Null:
break;
case TypedValue::Type::Bool:
this->bool_v = other.bool_v;
break;
case Type::Int:
this->int_v = other.int_v;
break;
case Type::Double:
this->double_v = other.double_v;
break;
case TypedValue::Type::String:
new (&string_v) std::string(std::move(other.string_v));
break;
case Type::List:
new (&list_v) std::vector<TypedValue>(std::move(other.list_v));
break;
case Type::Map:
new (&map_v) std::map<std::string, TypedValue>(std::move(other.map_v));
break;
case Type::Vertex:
new (&vertex_v) VertexAccessor(std::move(other.vertex_v));
break;
case Type::Edge:
new (&edge_v) EdgeAccessor(std::move(other.edge_v));
break;
case Type::Path:
new (&path_v) Path(std::move(other.path_v));
break;
}
other = TypedValue::Null;
}
TypedValue::operator PropertyValue() const {
switch (type_) {
case TypedValue::Type::Null:
@ -227,35 +309,57 @@ std::ostream &operator<<(std::ostream &os, const TypedValue &value) {
LOG(FATAL) << "Unsupported PropertyValue::Type";
}
#define DEFINE_TYPED_VALUE_ASSIGNMENT(type_param, typed_value_type, member) \
TypedValue &TypedValue::operator=(const type_param &other) { \
if (this->type_ == TypedValue::Type::typed_value_type) { \
this->member = other; \
} else { \
*this = TypedValue(other); \
} \
\
return *this; \
#define DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(type_param, typed_value_type, \
member) \
TypedValue &TypedValue::operator=(type_param other) { \
if (this->type_ == TypedValue::Type::typed_value_type) { \
this->member = other; \
} else { \
*this = TypedValue(other); \
} \
\
return *this; \
}
DEFINE_TYPED_VALUE_ASSIGNMENT(char *const, String, string_v)
DEFINE_TYPED_VALUE_ASSIGNMENT(int, Int, int_v)
DEFINE_TYPED_VALUE_ASSIGNMENT(bool, Bool, bool_v)
DEFINE_TYPED_VALUE_ASSIGNMENT(int64_t, Int, int_v)
DEFINE_TYPED_VALUE_ASSIGNMENT(double, Double, double_v)
DEFINE_TYPED_VALUE_ASSIGNMENT(std::string, String, string_v)
DEFINE_TYPED_VALUE_ASSIGNMENT(std::vector<TypedValue>, List, list_v)
DEFINE_TYPED_VALUE_ASSIGNMENT(TypedValue::value_map_t, Map, map_v)
DEFINE_TYPED_VALUE_ASSIGNMENT(VertexAccessor, Vertex, vertex_v)
DEFINE_TYPED_VALUE_ASSIGNMENT(EdgeAccessor, Edge, edge_v)
DEFINE_TYPED_VALUE_ASSIGNMENT(Path, Path, path_v)
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const char *, String, string_v)
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(int, Int, int_v)
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(bool, Bool, bool_v)
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(int64_t, Int, int_v)
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(double, Double, double_v)
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const std::string &, String, string_v)
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const std::vector<TypedValue> &, List,
list_v)
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const TypedValue::value_map_t &, Map, map_v)
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const VertexAccessor &, Vertex, vertex_v)
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const EdgeAccessor &, Edge, edge_v)
DEFINE_TYPED_VALUE_COPY_ASSIGNMENT(const Path &, Path, path_v)
#undef DEFINE_TYPED_VALUE_ASSIGNMENT
#undef DEFINE_TYPED_VALUE_COPY_ASSIGNMENT
#define DEFINE_TYPED_VALUE_MOVE_ASSIGNMENT(type_param, typed_value_type, \
member) \
TypedValue &TypedValue::operator=(type_param &&other) { \
if (this->type_ == TypedValue::Type::typed_value_type) { \
this->member = std::move(other); \
} else { \
*this = TypedValue(std::move(other)); \
} \
\
return *this; \
}
DEFINE_TYPED_VALUE_MOVE_ASSIGNMENT(std::string, String, string_v)
DEFINE_TYPED_VALUE_MOVE_ASSIGNMENT(std::vector<TypedValue>, List, list_v)
DEFINE_TYPED_VALUE_MOVE_ASSIGNMENT(TypedValue::value_map_t, Map, map_v)
DEFINE_TYPED_VALUE_MOVE_ASSIGNMENT(VertexAccessor, Vertex, vertex_v)
DEFINE_TYPED_VALUE_MOVE_ASSIGNMENT(EdgeAccessor, Edge, edge_v)
DEFINE_TYPED_VALUE_MOVE_ASSIGNMENT(Path, Path, path_v)
#undef DEFINE_TYPED_VALUE_MOVE_ASSIGNMENT
TypedValue &TypedValue::operator=(const TypedValue &other) {
if (this != &other) {
this->~TypedValue();
// set the type of this
DestroyValue();
type_ = other.type_;
switch (other.type_) {
@ -294,44 +398,92 @@ TypedValue &TypedValue::operator=(const TypedValue &other) {
return *this;
}
TypedValue &TypedValue::operator=(TypedValue &&other) {
if (this != &other) {
DestroyValue();
type_ = other.type_;
switch (other.type_) {
case TypedValue::Type::Null:
break;
case TypedValue::Type::Bool:
this->bool_v = other.bool_v;
break;
case TypedValue::Type::Int:
this->int_v = other.int_v;
break;
case TypedValue::Type::Double:
this->double_v = other.double_v;
break;
case TypedValue::Type::String:
new (&string_v) std::string(std::move(other.string_v));
break;
case TypedValue::Type::List:
new (&list_v) std::vector<TypedValue>(std::move(other.list_v));
break;
case TypedValue::Type::Map:
new (&map_v) std::map<std::string, TypedValue>(std::move(other.map_v));
break;
case TypedValue::Type::Vertex:
new (&vertex_v) VertexAccessor(std::move(other.vertex_v));
break;
case TypedValue::Type::Edge:
new (&edge_v) EdgeAccessor(std::move(other.edge_v));
break;
case TypedValue::Type::Path:
new (&path_v) Path(std::move(other.path_v));
break;
}
other = TypedValue::Null;
}
return *this;
}
const TypedValue TypedValue::Null = TypedValue();
TypedValue::~TypedValue() {
void TypedValue::DestroyValue() {
switch (type_) {
// destructor for primitive types does nothing
case Type::Null:
case Type::Bool:
case Type::Int:
case Type::Double:
return;
case Type::Null:
case Type::Bool:
case Type::Int:
case Type::Double:
break;
// we need to call destructors for non primitive types since we used
// placement new
case Type::String:
// Clang fails to compile ~std::string. It seems it is a bug in some
// versions of clang. using namespace std statement solves the issue.
using namespace std;
string_v.~string();
return;
case Type::List:
using namespace std;
list_v.~vector<TypedValue>();
return;
case Type::Map:
using namespace std;
map_v.~map<std::string, TypedValue>();
return;
case Type::Vertex:
vertex_v.~VertexAccessor();
return;
case Type::Edge:
edge_v.~EdgeAccessor();
return;
case Type::Path:
path_v.~Path();
return;
case Type::String:
// Clang fails to compile ~std::string. It seems it is a bug in some
// versions of clang. using namespace std statement solves the issue.
using namespace std;
string_v.~string();
break;
case Type::List:
using namespace std;
list_v.~vector<TypedValue>();
break;
case Type::Map:
using namespace std;
map_v.~map<std::string, TypedValue>();
break;
case Type::Vertex:
vertex_v.~VertexAccessor();
break;
case Type::Edge:
edge_v.~EdgeAccessor();
break;
case Type::Path:
path_v.~Path();
break;
}
LOG(FATAL) << "Unsupported TypedValue::Type";
type_ = TypedValue::Type::Null;
}
TypedValue::~TypedValue() {
DestroyValue();
}
/**
@ -651,7 +803,7 @@ bool TypedValue::BoolEqual::operator()(const TypedValue &lhs,
default:
LOG(FATAL)
<< "Equality between two TypedValues resulted in something other "
"then Null or bool";
"than Null or bool";
}
}

View File

@ -6,6 +6,7 @@
#include <memory>
#include <string>
#include <unordered_set>
#include <utility>
#include <vector>
#include "query/path.hpp"
@ -55,6 +56,7 @@ class TypedValue
* in the floating point domain (TypedValue operator== behaves the same).
* */
using unordered_set = std::unordered_set<TypedValue, Hash, BoolEqual>;
using value_map_t = std::map<std::string, TypedValue>;
/** Private default constructor, makes Null */
TypedValue() : type_(Type::Null) {}
@ -77,6 +79,9 @@ class TypedValue
// single static reference to Null, used whenever Null should be returned
static const TypedValue Null;
TypedValue(const TypedValue &other);
TypedValue(TypedValue &&other);
// constructors for primitive types
TypedValue(bool value) : type_(Type::Bool) { bool_v = value; }
TypedValue(int value) : type_(Type::Int) { int_v = value; }
@ -86,7 +91,7 @@ class TypedValue
// conversion function to PropertyValue
explicit operator PropertyValue() const;
/// constructors for non-primitive types
// copy constructors for non-primitive types
TypedValue(const std::string &value) : type_(Type::String) {
new (&string_v) std::string(value);
}
@ -109,33 +114,50 @@ class TypedValue
TypedValue(const Path &path) : type_(Type::Path) { new (&path_v) Path(path); }
TypedValue(const PropertyValue &value);
/**
* There are all sorts of explicit assignments here because this way we avoid
* destructor and constructor of TypedValue for creating intermediary values,
* and can fill the typed value storage directly if it has the same underlying
* type.
*/
#define DECLARE_TYPED_VALUE_ASSIGNMENT(type_param) \
TypedValue &operator=(const type_param &other);
// move constructors for non-primitive types
TypedValue(std::string &&value) : type_(Type::String) {
new (&string_v) std::string(std::move(value));
}
TypedValue(std::vector<TypedValue> &&value) : type_(Type::List) {
new (&list_v) std::vector<TypedValue>(std::move(value));
}
TypedValue(std::map<std::string, TypedValue> &&value) : type_(Type::Map) {
new (&map_v) std::map<std::string, TypedValue>(std::move(value));
}
TypedValue(VertexAccessor &&vertex) : type_(Type::Vertex) {
new (&vertex_v) VertexAccessor(std::move(vertex));
}
TypedValue(EdgeAccessor &&edge) : type_(Type::Edge) {
new (&edge_v) EdgeAccessor(std::move(edge));
}
TypedValue(Path &&path) : type_(Type::Path) {
new (&path_v) Path(std::move(path));
}
TypedValue(PropertyValue &&value);
using value_map_t = std::map<std::string, TypedValue>;
// Don't delete char * const assignment because char* strings will be assigned
// using boolean assignment (not good).
DECLARE_TYPED_VALUE_ASSIGNMENT(char *const)
DECLARE_TYPED_VALUE_ASSIGNMENT(int)
DECLARE_TYPED_VALUE_ASSIGNMENT(bool)
DECLARE_TYPED_VALUE_ASSIGNMENT(int64_t)
DECLARE_TYPED_VALUE_ASSIGNMENT(double)
DECLARE_TYPED_VALUE_ASSIGNMENT(std::string)
DECLARE_TYPED_VALUE_ASSIGNMENT(std::vector<TypedValue>)
DECLARE_TYPED_VALUE_ASSIGNMENT(TypedValue::value_map_t)
DECLARE_TYPED_VALUE_ASSIGNMENT(VertexAccessor)
DECLARE_TYPED_VALUE_ASSIGNMENT(EdgeAccessor)
DECLARE_TYPED_VALUE_ASSIGNMENT(Path)
DECLARE_TYPED_VALUE_ASSIGNMENT(TypedValue)
#undef DECLARE_TYPED_VALUE_ASSIGNMENT
// copy assignment operators
TypedValue &operator=(const char *);
TypedValue &operator=(int);
TypedValue &operator=(bool);
TypedValue &operator=(int64_t);
TypedValue &operator=(double);
TypedValue &operator=(const std::string &);
TypedValue &operator=(const std::vector<TypedValue> &);
TypedValue &operator=(const TypedValue::value_map_t &);
TypedValue &operator=(const VertexAccessor &);
TypedValue &operator=(const EdgeAccessor &);
TypedValue &operator=(const Path &);
TypedValue &operator=(const TypedValue &);
// move assignment operators
TypedValue &operator=(std::string &&);
TypedValue &operator=(std::vector<TypedValue> &&);
TypedValue &operator=(TypedValue::value_map_t &&);
TypedValue &operator=(VertexAccessor &&);
TypedValue &operator=(EdgeAccessor &&);
TypedValue &operator=(Path &&);
TypedValue &operator=(TypedValue &&);
TypedValue(const TypedValue &other);
~TypedValue();
Type type() const { return type_; }
@ -189,6 +211,8 @@ class TypedValue
friend std::ostream &operator<<(std::ostream &stream, const TypedValue &prop);
private:
void DestroyValue();
// storage for the value of the property
union {
bool bool_v;

View File

@ -10,8 +10,8 @@
#include "utils/total_ordering.hpp"
/**
* Encapsulation of a value and it's type encapsulated in a class that has no
* compiled-time info about that type.
* Encapsulation of a value and its type in a class that has no compile-time
* info about the type.
*
* Values can be of a number of predefined types that are enumerated in
* PropertyValue::Type. Each such type corresponds to exactly one C++ type.