Skip to content

Commit ea3be68

Browse files
committed
MB-59746 [2/2]: Optimise space needed to store locked_CAS
Optimise the representation of the 'cas' and 'locked_cas' fields, so they always consume 8B in StoredValue, at the cost of some complexity. We need to store one CAS value when the SV is unlocked (common), and two CAS values when locked (rare). We want to dto this without having to unncessarily inflate the size of _all_ StoredValue objects, which the current basic impl does (by always storing both cas and locked_cas). Logically what we want is a the 'cas' member to be variant which is either one or two CAS values: std::variant<uint64_t, CasPair> However this has sizeof(max_sizeof(T...)) + 1 = 17Bytes on 64bit systems. To achive this funcionality using only 8B, we use two optimisations: 1. Use an external variable (casIsSeparate) as the tag of the variant (saves 1 Byte). 2. For the CasPair variant, allocate the CasPair on the heap and just store a ptr to it in the variant (save 8 Bytes). (1) Somewhat complicates the code, as we need to manage the tag manually - but that's the price we pay for avoiding the extra byte std::variant would have.. (2) comes at the cost of an additional 16B heap allocation for locked documents (in addition to the fixed 8B footprint of this field), but given locked documents are very rare compared to unlocked ones, this should overall be cheaper than spending 16B on every SV. Change-Id: I8b95a743ef041af3d5566d13087c60213811e359 Reviewed-on: https://review.couchbase.org/c/kv_engine/+/201511 Reviewed-by: Vesko Karaganev <[email protected]> Tested-by: Dave Rigby <[email protected]> Well-Formed: Restriction Checker
1 parent 64e68cf commit ea3be68

File tree

3 files changed

+219
-42
lines changed

3 files changed

+219
-42
lines changed

engines/ep/src/stored-value.cc

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ StoredValue::StoredValue(const Item& itm,
3636
bool isOrdered)
3737
: value(itm.getValue()),
3838
chain_next_or_replacement(std::move(n)),
39-
cas(itm.getCas()),
4039
bySeqno(itm.getBySeqno()),
4140
exptime(itm.getExptime()),
4241
flags(itm.getFlags()),
4342
revSeqno(itm.getRevSeqno()),
4443
datatype(itm.getDataType()),
4544
ordered(isOrdered),
4645
deletionSource(0),
47-
committed(static_cast<uint8_t>(CommittedState::CommittedViaMutation)) {
46+
committed(static_cast<uint8_t>(CommittedState::CommittedViaMutation)),
47+
casIsSeparate(0) {
4848
// Initialise bit fields
4949
setDeletedPriv(itm.isDeleted());
5050
setResident(!isTempItem());
@@ -53,6 +53,8 @@ StoredValue::StoredValue(const Item& itm,
5353
setAge(0);
5454
// dirty initialised below
5555

56+
setCas(itm.getCas());
57+
5658
// Placement-new the key which lives in memory directly after this
5759
// object.
5860
new (key()) SerialisedDocKey(itm.getKey());
@@ -76,26 +78,30 @@ StoredValue::StoredValue(const Item& itm,
7678

7779
StoredValue::~StoredValue() {
7880
ObjectRegistry::onDeleteStoredValue(this);
81+
// If still has a locked CAS, must clear this to delete the CasPair before
82+
// destroying StoredValue.
83+
clearLockedCas();
7984
}
8085

8186
StoredValue::StoredValue(const StoredValue& other, UniquePtr n, EPStats& stats)
8287
: value(other.value), // Implicitly also copies the frequency counter
8388
chain_next_or_replacement(std::move(n)),
84-
cas(other.cas),
8589
bySeqno(other.bySeqno),
8690
lock_expiry_or_delete_or_complete_time(
8791
other.lock_expiry_or_delete_or_complete_time),
8892
exptime(other.exptime),
8993
flags(other.flags),
9094
revSeqno(other.revSeqno),
9195
datatype(other.datatype),
92-
ordered(other.ordered) {
96+
ordered(other.ordered),
97+
casIsSeparate(0) {
9398
setDirty(other.isDirty());
9499
setDeletedPriv(other.isDeleted());
95100
setResident(other.isResident());
96101
setStale(false);
97102
setCommitted(other.getCommitted());
98103
setAge(0);
104+
setCas(other.getCas());
99105
// Placement-new the key which lives in memory directly after this
100106
// object.
101107
StoredDocKey sKey(other.getKey());
@@ -122,7 +128,7 @@ void StoredValue::ejectValue() {
122128

123129
void StoredValue::restoreValue(const Item& itm) {
124130
if (isTempInitialItem() || isTempDeletedItem()) {
125-
cas = itm.getCas();
131+
setCas(itm.getCas());
126132
flags = itm.getFlags();
127133
exptime = itm.getExptime();
128134
revSeqno = itm.getRevSeqno();
@@ -144,7 +150,7 @@ void StoredValue::restoreValue(const Item& itm) {
144150
}
145151

146152
void StoredValue::restoreMeta(const Item& itm) {
147-
cas = itm.getCas();
153+
setCas(itm.getCas());
148154
flags = itm.getFlags();
149155
datatype = itm.getDataType();
150156
exptime = itm.getExptime();
@@ -158,6 +164,24 @@ void StoredValue::restoreMeta(const Item& itm) {
158164
setCommitted(itm.getCommitted());
159165
}
160166

167+
void StoredValue::lock(rel_time_t expiry, uint64_t lockedCas) {
168+
if (isDeleted()) {
169+
// Cannot lock Deleted items.
170+
throw std::logic_error("StoredValue::lock: Called on Deleted item");
171+
}
172+
setLockedCas(lockedCas);
173+
lock_expiry_or_delete_or_complete_time.lock_expiry = expiry;
174+
}
175+
176+
void StoredValue::unlock() {
177+
if (isDeleted()) {
178+
// Deleted items are not locked - just skip.
179+
return;
180+
}
181+
clearLockedCas();
182+
lock_expiry_or_delete_or_complete_time.lock_expiry = 0;
183+
}
184+
161185
size_t StoredValue::uncompressedValuelen() const {
162186
if (!value) {
163187
return 0;
@@ -266,7 +290,7 @@ bool StoredValue::operator==(const StoredValue& other) const {
266290
auto& otherOsv = static_cast<const OrderedStoredValue&>(other);
267291
orderedEqual = osv.prepareSeqno == otherOsv.prepareSeqno;
268292
}
269-
return (cas == other.cas && revSeqno == other.revSeqno &&
293+
return (getCas() == other.getCas() && revSeqno == other.revSeqno &&
270294
bySeqno == other.bySeqno &&
271295
lock_expiry_or_delete_or_complete_time.lock_expiry ==
272296
other.lock_expiry_or_delete_or_complete_time.lock_expiry &&
@@ -335,7 +359,7 @@ void StoredValue::setValueImpl(const Item& itm) {
335359
flags = itm.getFlags();
336360
datatype = itm.getDataType();
337361
bySeqno = itm.getBySeqno();
338-
cas = itm.getCas();
362+
setCas(itm.getCas());
339363
lock_expiry_or_delete_or_complete_time.lock_expiry = 0;
340364
exptime = itm.getExptime();
341365
revSeqno = itm.getRevSeqno();
@@ -355,6 +379,57 @@ void StoredValue::setValueImpl(const Item& itm) {
355379
setCommitted(itm.getCommitted());
356380
}
357381

382+
StoredValue::CasPair StoredValue::getCasPair() const {
383+
switch (getCasEncoding()) {
384+
case CasEncoding::InlineSingle:
385+
// `cas' encodes the original CAS value, locked CAS is empty.
386+
CasPair pair;
387+
pair.originalCAS = cas.single;
388+
return pair;
389+
case CasEncoding::SeparateDouble:
390+
// 'cas' points to a CasPair holding two CAS values:
391+
return *cas.pair;
392+
}
393+
folly::assume_unreachable();
394+
}
395+
396+
void StoredValue::setLockedCas(uint64_t lockedCas) {
397+
// Switch to SeparateDouble encoding (if not already)
398+
switch (getCasEncoding()) {
399+
case CasEncoding::InlineSingle: {
400+
// Create separate CasPair.
401+
const auto originalCas = cas.single;
402+
cas.pair = new CasPair{originalCas, lockedCas};
403+
casIsSeparate = 1;
404+
return;
405+
}
406+
case CasEncoding::SeparateDouble:
407+
// Format already correct, just update lockedCas.
408+
cas.pair->lockedCAS = lockedCas;
409+
return;
410+
}
411+
folly::assume_unreachable();
412+
}
413+
414+
void StoredValue::clearLockedCas() {
415+
// Switch to InlineSingle encoding (if not already)
416+
switch (getCasEncoding()) {
417+
case CasEncoding::InlineSingle:
418+
// Format already correct (double unlock?) -
419+
// for robustness allow this.
420+
return;
421+
case CasEncoding::SeparateDouble: {
422+
// Free separate pair, put originalCas back.
423+
const auto originalCas = cas.pair->originalCAS;
424+
delete cas.pair;
425+
cas.single = originalCas;
426+
casIsSeparate = 0;
427+
return;
428+
}
429+
}
430+
folly::assume_unreachable();
431+
}
432+
358433
bool StoredValue::compressValue() {
359434
if (!mcbp::datatype::is_snappy(datatype)) {
360435
// Attempt compression only if datatype indicates
@@ -395,7 +470,7 @@ std::optional<item_info> StoredValue::getItemInfo(uint64_t vbuuid) const {
395470
}
396471

397472
item_info info;
398-
info.cas = cas;
473+
info.cas = getCas();
399474
info.vbucket_uuid = vbuuid;
400475
info.seqno = bySeqno;
401476
info.exptime = exptime;
@@ -446,7 +521,7 @@ void to_json(nlohmann::json& json, const StoredValue& sv) {
446521
json["prepareSeqno"] = static_cast<uint64_t>(osv.prepareSeqno);
447522
}
448523

449-
json["cas"] = sv.cas;
524+
json["cas"] = sv.getCas();
450525
json["rev"] = static_cast<uint64_t>(sv.revSeqno);
451526
json["seqno"] = sv.bySeqno.load();
452527
json["l/e/d/c time"] =
@@ -574,8 +649,8 @@ std::ostream& operator<<(std::ostream& os, const StoredValue& sv) {
574649
os << std::dec << "seq:" << uint64_t(sv.getBySeqno())
575650
<< " rev:" << sv.getRevSeqno();
576651
os << " cas:" << sv.getCas();
577-
if (sv.isLocked(now)) {
578-
os << " locked_cas:" << sv.locked_cas;
652+
if (sv.getCasEncoding() == StoredValue::CasEncoding::SeparateDouble) {
653+
os << " locked_cas:" << sv.getCasPair().lockedCAS;
579654
}
580655
os << " key:\"" << sv.getKey() << "\"";
581656
if (sv.isOrdered() && sv.isDeleted()) {

engines/ep/src/stored-value.h

Lines changed: 103 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ class StoredValue {
451451
* @return the cas ID
452452
*/
453453
uint64_t getCas() const {
454-
return cas;
454+
return getCasPair().originalCAS;
455455
}
456456

457457
/**
@@ -462,44 +462,37 @@ class StoredValue {
462462
* @param curtime The current time, used to check if lock has expired
463463
*/
464464
uint64_t getCasForWrite(rel_time_t curtime) const {
465+
auto pair = getCasPair();
465466
if (isLocked(curtime)) {
466-
return locked_cas;
467+
return pair.lockedCAS;
467468
}
468-
return cas;
469+
return pair.originalCAS;
469470
}
470471

471472
/**
472473
* Set a new CAS ID.
473474
*/
474475
void setCas(uint64_t c) {
475-
cas = c;
476+
switch (getCasEncoding()) {
477+
case CasEncoding::InlineSingle:
478+
cas.single = c;
479+
break;
480+
case CasEncoding::SeparateDouble:
481+
cas.pair->originalCAS = c;
482+
break;
483+
}
476484
}
477485

478486
/**
479487
* Lock this item until the given time, setting the locked CAS to
480488
* the specified value.
481489
*/
482-
void lock(rel_time_t expiry, uint64_t cas) {
483-
if (isDeleted()) {
484-
// Cannot lock Deleted items.
485-
throw std::logic_error(
486-
"StoredValue::lock: Called on Deleted item");
487-
}
488-
lock_expiry_or_delete_or_complete_time.lock_expiry = expiry;
489-
locked_cas = cas;
490-
}
490+
void lock(rel_time_t expiry, uint64_t cas);
491491

492492
/**
493493
* Unlock this item.
494494
*/
495-
void unlock() {
496-
if (isDeleted()) {
497-
// Deleted items are not locked - just skip.
498-
return;
499-
}
500-
lock_expiry_or_delete_or_complete_time.lock_expiry = 0;
501-
locked_cas = 0;
502-
}
495+
void unlock();
503496

504497
/**
505498
* True if this item has an ID.
@@ -1016,6 +1009,58 @@ class StoredValue {
10161009
deletionSource = static_cast<uint8_t>(delSource);
10171010
}
10181011

1012+
/**
1013+
* How are the CAS value(s) currently encoded?
1014+
*/
1015+
enum class CasEncoding {
1016+
// Single CAS stored inline in the StoredValue.
1017+
InlineSingle,
1018+
// Double CAS (original and locked) stored separarely in a
1019+
// heap-allocated CasPair, StoredValue holds a ptr to them.
1020+
SeparateDouble,
1021+
};
1022+
1023+
/**
1024+
* When a document is locked, holds both the CAS of the last mutation
1025+
* (the pre-lock, original CAS), and the CAS generated by
1026+
* VBucket::getLocked() which is needed to mutate the document while locked
1027+
* or unlock it.
1028+
*
1029+
* This type is used only for documents which have been locked, and is
1030+
* heap-allocated and pointed-to by the CasPair* variant of
1031+
* StoredValue::cas.
1032+
*/
1033+
struct CasPair {
1034+
uint64_t originalCAS;
1035+
uint64_t lockedCAS;
1036+
};
1037+
1038+
/// Returns the current encoding of the CAS value(s).
1039+
CasEncoding getCasEncoding() const {
1040+
return casIsSeparate ? CasEncoding::SeparateDouble
1041+
: CasEncoding::InlineSingle;
1042+
}
1043+
1044+
/**
1045+
* Returns both CAS values associated with the StoredValue; the
1046+
* original CAS and the locked CAS.
1047+
* If the document doesn't currently have two CAS values (i.e. was not
1048+
* locked) then lockedCAS will be zero.
1049+
*/
1050+
CasPair getCasPair() const;
1051+
1052+
/**
1053+
* Sets the locked CAS to the specified value. This changes the
1054+
* CAS encoding to separate double if not already.
1055+
*/
1056+
void setLockedCas(uint64_t lockedCas);
1057+
1058+
/**
1059+
* Clears (discards) the locked CAS. This changes the CAS encoding
1060+
* to inline single if not already.
1061+
*/
1062+
void clearLockedCas();
1063+
10191064
friend class StoredValueFactory;
10201065

10211066
/**
@@ -1074,11 +1119,40 @@ class StoredValue {
10741119
// @todo: Re-factoring of the UniquePtr management is needed to safely use
10751120
// the tag.
10761121
UniquePtr chain_next_or_replacement; // 8 bytes (2-byte tag, 6 byte address)
1077-
uint64_t cas; //!< CAS identifier.
1078-
// The CAS to use to modify/unlock a locked document. Stored separately from
1079-
// 'cas' as we must preserve the original CAS in the event the locked
1080-
// document is unlocked without being modified.
1081-
uint64_t locked_cas;
1122+
1123+
// Space-optimised representation of one or two CAS values. Always consumes
1124+
// 8B in StoredValue, at the cost of some complexity.
1125+
//
1126+
// We need to store one CAS value when the SV is unlocked (common), and two
1127+
// CAS values when locked (rare). We want to do this without having to
1128+
// unncessarily inflate the size of _all_ StoredValue objects, which would
1129+
// occur with a basic implementation along the lines of:
1130+
//
1131+
// std::variant<uint64_t, CasPair>
1132+
//
1133+
// which has sizeof(max_sizeof(T...)) + 1 = 17Bytes on 64bit systems.
1134+
// To achive this funcionality using only 8B, we use two optimisations:
1135+
// 1. Use an external variable (casIsSeparate) as the tag of the variant
1136+
// (saves 1 Byte).
1137+
// 2. For the CasPair variant, allocate the CasPair on the heap and just
1138+
// store a ptr to it in the variant (save 8 Bytes).
1139+
//
1140+
// (1) Somewhat complicates the code, as we need to manage the tag manually
1141+
// - but that's the price we pay for avoiding the extra byte std::variant
1142+
// would have..
1143+
// (2) comes at the cost of an additional 16B heap allocation for locked
1144+
// documents (in addition to the fixed 8B footprint of this field), but
1145+
// given locked documents are very rare compared to unlocked ones, this
1146+
// should overall be cheaper than spending 16B on every SV.
1147+
union Cas {
1148+
uint64_t single;
1149+
CasPair* pair;
1150+
1151+
Cas() : single(0) {}
1152+
Cas(uint64_t c) : single(c) {
1153+
}
1154+
} cas;
1155+
10821156
// bySeqno is atomic primarily for TSAN, which would flag that we write/read
10831157
// this in ephemeral backfills with different locks (which is true, but the
10841158
// access is we believe actually safe)
@@ -1139,6 +1213,9 @@ class StoredValue {
11391213
uint8_t deletionSource : 1;
11401214
/// 3-bit value which encodes the CommittedState of the StoredValue
11411215
uint8_t committed : 3;
1216+
// If set then CAS is currently encoded as SeparateDouble, if clear
1217+
// then CAS currently encoded as InlineSingle.
1218+
uint8_t casIsSeparate : 1;
11421219

11431220
friend std::ostream& operator<<(std::ostream& os, const StoredValue& sv);
11441221
friend void to_json(nlohmann::json& json, const StoredValue& sv);

0 commit comments

Comments
 (0)