-
Notifications
You must be signed in to change notification settings - Fork 290
428 - Use global primitive data types instead of local data type caches #334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This looks great! My only gripe is that when adding a new type to the system, we have to update three files: the CASS_VALUE_TYPE_MAPPING in cassandra.h and the two .gperf files. The former actually has some of the same info as the value_types_by_cql.gperf file. Consider this alternative:
If it's an STL map, we'll have log(n) access, which is, of course, worse than the O(1) hash access provided by gperf. If that's a concern, maybe there's a general hash-map impl we can leverage. It may not be perfect hashing, but I think that would be good enough. |
src/data_type.cpp
Outdated
DataTypeInitializer() { | ||
// Add a reference so that the memory is never deleted | ||
#define XX_VALUE_TYPE(name, type, cql) (new(&DataType::native_types_[name]) DataType(name))->inc_ref(); | ||
CASS_VALUE_TYPE_MAPPING(XX_VALUE_TYPE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you're instantiating into the class array (which is already allocated), under what circumstance do you have to worry about the memory being deallocated? Process exit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't allocate heap memory. This is a "placement new".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I get that this is "placement new", but I don't understand why the inc_ref is needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I avoids the reference count from ever reaching zero because the memory is statically allocated. If the reference count reaches zero then it would be freed using delete
which would be incorrect.
src/data_type.cpp
Outdated
ValueTypeByClassMapping* mapping = | ||
ValueTypeByClass::in_word_set(name.c_str(), name.length()); | ||
if (mapping == NULL) return DataType::NIL; | ||
return DataType::ConstPtr(&native_types_[mapping->value_type]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the above inc_ref keep the ConstPtr from deallocating the array's memory (when the ConstPtr eventually is destroyed)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
src/generate_value_types.sh
Outdated
#!/bin/sh | ||
# 'register' is a deprecated keyword in C++1x | ||
gperf -t value_types_by_class.gperf | sed 's/register //g' > value_types_by_class.hpp | ||
gperf -t value_types_by_cql.gperf | sed 's/register //g' > value_types_by_cql.hpp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a user calls the script from outside of the src dir, the generated files won't go to the src directory. How about something like this (untested):
DIRNAME=`dirname "$0"`
for i in value_types_by_class value_types_by_cql ; do
gperf -t "$DIRNAME/$i.gperf" | sed 's/register //g' > "$DIRNAME/$i.hpp"
done
No one should be insane enough to use spaces in their directory paths, but the extra quoting above handles it if they do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree with the no spaces in directory paths; Windows still loves spaces and a good number of those users still utilizes spaces in their paths especially when their user directory is their fullname which may contain spaces.
These are my concerns:
- Using a shell script to generate these files requires that we also make a batch file for Windows; would prefer that we utilize CMake to handle the generation instead
- Committing generated files could lead to a situation where we have forgotten to manually generate the files and commit; granted chances are low
- Not making this an automated step during the build process turns this into a manual process when we should probably automate it
- and 3) are kind of the same and I am not 100% against keeping this generation manual and committing the files when required as the amount of times these will change is extremely low.
I'm not concerned about O(log(n)) vs O(1). I'm trying to avoid heap allocation from being done in the static context. We did this a bunch when we first release the driver and had clients that had issues. I'm trying to remember what the issues were (I'll get back to you when I remember or find the issues). At first I wasn't even going to use static initialization (because this can have issues too). I was going to use a pattern similar to SSL's initialization, but this seems to be simple enough of a use case and doesn't have any heap allocation. |
Side note: We could have moved |
I remember the issue. Allocating heap memory in static initialization prevents users from allocating that memory using their custom heap allocator. Static initialization usually happens when the library is loaded, before the client application has had a chance to run, so the client application is unable to set their allocator for memory allocated during that prior static initialization. Custom allocation is something we plan to support, in short order, because it has been requested by several major users. The issue: https://datastax-oss.atlassian.net/browse/CPP-360 |
I agree, it's not great having that same information duplicated in a few files. Maybe those .gperf files can be generated on-the-fly, as an intermediate step, using the information from |
Heh, Since you both thumbs up it do you want to write the generator? :) |
src/data_type.cpp
Outdated
|
||
DataType::ConstPtr SimpleDataTypeCache::by_value_type(uint16_t value_type) { | ||
if (value_type == CASS_VALUE_TYPE_UNKNOWN || | ||
value_type <= CASS_VALUE_TYPE_LAST_ENTRY) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be >=?
src/data_type.hpp
Outdated
// etc) so these data types don't need to be allocated/deallocated over and over. | ||
// `DataType` is reference counted so it could lead to mulitple threads modifying | ||
// a shared reference count. To mitigate this sharing, thread IDs are used | ||
// to distribute mulitple copies of the same data type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mulitple => multiple
Love the comment blocks here and above.
518d8fc
to
bd11338
Compare
* Made the string to type mapping for C* types more generalized (it was only meant to be used by schema metadata) and faster using a statically allocated hash table. * Realized that using `CASS_<type>_MAP()` was bad naming as "MAP" could refer to a "map" data/value type. Changed it to `CASS_<type>_MAPPING()`. Backwards compatible macros were added to avoid breaking existing applications. * Realized `FixedVector` was bad naming. `SmallVector` better describes its purpose.
Fixed typo.
This is finally ready to review again. This iteration doesn't require having value type information in multiple places and is simpler. This uses |
} | ||
DataType::ConstPtr& data_type = cache_[value_type]; | ||
if (!data_type) { | ||
data_type = DataType::ConstPtr( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't cache_ be updated here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a reference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, duh! Makes sense.
|
||
class SimpleDataTypeCache { | ||
public: | ||
const DataType::ConstPtr& by_class(StringRef name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not build up this cache at the beginning of time similar to ValueTypes and make these methods static (and make cache_ static)? Then you wouldn't need to pass a SimpleTypeTypeCache to various methods below...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm really concerned about sharing reference counted objects on multiple threads. With this method we get some sharing without causing multiple cores to thrash on those shared reference counts.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need reference counting for this? What if it's a singleton initialized at the beginning of time and consumers get the elements they need? The consumers don't even try to delete what they've got, and when the application terminates, the singleton will be destroyed then. by_class and similar methods could return a const DataType&
or const DataType*
; the former would make it less likely that a consumer would try to destroy the DataType.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is the external API and other places in the code treat more complex and unique data types (collections, UDTs, tuples, etc.) as generic DataType
. Even if we return these as const DataType&
or const DataType*
they're going to eventually be put into a DataType::ConstPtr
which is going to modify the shared reference count.
// which can be found at the bottom of "sparesehash/internal/densehashtable.h". | ||
#define OCCUPANCY_PCT 50 | ||
|
||
#define MIN_BUCKETS(N) STATIC_NEXT_POW_2((((N * 100) / OCCUPANCY_PCT) + 1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reasoning behind this calculation of MIN_BUCKETS?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to statically allocate enough buckets to hold N items without requiring additional allocations. A hash table doesn't just allocate N buckets to hold N items because of hash collisions so it allocates a number of buckets over the current number of items (controlled by a load factor) to compensate for that.
OCCUPANCY_PCT
is dense_hash_map
's load factor and it needs the number of buckets to be a power of two because it uses a mask and &
to wrap around instead of using modulo (which is expensive).
This calculates the number of buckets considering the load factor: (N * 100) / OCCUPANCY_PCT
(The + 1
is to allocate at least one bucket) then it rounds up to the next power of two for the wrap around trick.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coolness. Makes sense. They key thing I was missing was that the wrap around is done by mask and requires num-buckets to be a power of 2.
typedef sparsehash::dense_hash_map<K, V, HashFcn, EqualKey, Allocator> HashMap; | ||
|
||
SmallDenseHashMap() | ||
: HashMap(N, typename HashMap::hasher(), typename HashMap::key_equal(), Allocator(&fixed_)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use typename
here but not in the other constructor? Also, aren't these actual values, not types, so we shouldn't have typename
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixing compiler errors. Sometimes C++ compiler get confused and need told that a name refers to a type. Perhaps it should be added to both, and the second constructor didn't error because it's not being currently used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, this is parsed as (typename HashMap::hasher)()
not typename (HashMap::hasher())
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohhh... that does make more sense.
BOOST_CHECK(strlen(klass) == 0 || \ | ||
cass::ValueTypes::by_class(klass) == name); \ | ||
|
||
CASS_VALUE_TYPE_MAPPING(XX_VALUE_TYPE) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very neat use of the macro!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think all questions have been resolved, except about the ref counting in SimpleDataTypeCache.
} | ||
DataType::ConstPtr& data_type = cache_[value_type]; | ||
if (!data_type) { | ||
data_type = DataType::ConstPtr( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, duh! Makes sense.
|
||
class SimpleDataTypeCache { | ||
public: | ||
const DataType::ConstPtr& by_class(StringRef name) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need reference counting for this? What if it's a singleton initialized at the beginning of time and consumers get the elements they need? The consumers don't even try to delete what they've got, and when the application terminates, the singleton will be destroyed then. by_class and similar methods could return a const DataType&
or const DataType*
; the former would make it less likely that a consumer would try to destroy the DataType.
// which can be found at the bottom of "sparesehash/internal/densehashtable.h". | ||
#define OCCUPANCY_PCT 50 | ||
|
||
#define MIN_BUCKETS(N) STATIC_NEXT_POW_2((((N * 100) / OCCUPANCY_PCT) + 1)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coolness. Makes sense. They key thing I was missing was that the wrap around is done by mask and requires num-buckets to be a power of 2.
typedef sparsehash::dense_hash_map<K, V, HashFcn, EqualKey, Allocator> HashMap; | ||
|
||
SmallDenseHashMap() | ||
: HashMap(N, typename HashMap::hasher(), typename HashMap::key_equal(), Allocator(&fixed_)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohhh... that does make more sense.
Merged at 3c55e33 |
No description provided.