Skip to content

Commit 6cf1240

Browse files
committed
MDEV-21423 - lock-free trx_sys get performance regression cause by lf_find and ut_delay
Under high concurrency, MVCC snapshot creation may spend a significant amount of time in lf_hash_iterate()/lfind() while collecting active read-write transaction identifiers. This overhead is particularly visible in sysbench oltp_read_write with transaction-isolation=READ-COMMITTED. Iteration cost becomes high due to significant TLB thrashing and poor memory locality in this hot code path because snapshot creation touches many rw_trx_hash nodes distributed across memory, including dummy nodes that are irrelevant for snapshot construction. In addition, traversing LF_HASH requires issuing heavyweight memory barriers. This is a performance regression after 53cc9aa, which changed MVCC snapshot creation to scan LF_HASH instead of maintaining a global sorted vector protected by the global mutex. Add trx_sys.rw_trx_ids, a compact traversal-friendly vector of active read-write transaction identifiers and serialization numbers optimized for MVCC snapshot creation, while rw_trx_hash remains responsible for transaction lookup. The vector may contain empty slots corresponding to idle or read-only transactions that currently do not own a read-write transaction identifier. Such slots are skipped by snapshot creation. This reduces traversal overhead during MVCC snapshot creation by improving memory locality, reducing TLB pressure, and avoiding repeated memory barriers required for rw_trx_hash traversal.
1 parent c8bfb4d commit 6cf1240

9 files changed

Lines changed: 193 additions & 85 deletions

File tree

storage/innobase/include/trx0purge.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ Created 3/26/1996 Heikki Tuuri
3737
Remove the undo log segment from the rseg slot if it is too big for reuse.
3838
@param[in] trx transaction
3939
@param[in,out] undo undo log
40-
@param[in,out] mtr mini-transaction */
40+
@param[in,out] mtr mini-transaction
41+
@param[in] end transaction serialisation number */
4142
void
42-
trx_purge_add_undo_to_history(const trx_t* trx, trx_undo_t*& undo, mtr_t* mtr);
43+
trx_purge_add_undo_to_history(const trx_t* trx, trx_undo_t*& undo, mtr_t* mtr,
44+
trx_id_t end);
4345

4446
/**
4547
Remove unnecessary history data from rollback segments. NOTE that when this

storage/innobase/include/trx0sys.h

Lines changed: 169 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -340,14 +340,6 @@ struct rw_trx_hash_element_t
340340

341341

342342
trx_id_t id; /* lf_hash_init() relies on this to be first in the struct */
343-
344-
/**
345-
Transaction serialization number.
346-
347-
Assigned shortly before the transaction is moved to COMMITTED_IN_MEMORY
348-
state. Initially set to TRX_ID_MAX.
349-
*/
350-
Atomic_counter<trx_id_t> no;
351343
trx_t *trx;
352344
srw_mutex mutex;
353345
};
@@ -443,7 +435,6 @@ class rw_trx_hash_t
443435
ut_ad(element->trx == 0);
444436
element->trx= trx;
445437
element->id= trx->id;
446-
element->no= TRX_ID_MAX;
447438
trx->rw_trx_hash_element= element;
448439
}
449440

@@ -512,7 +503,6 @@ class rw_trx_hash_t
512503
if (element->trx)
513504
validate_element(element->trx);
514505
element->mutex.wr_unlock();
515-
ut_ad(element->id < element->no);
516506
return arg->action(element, arg->argument);
517507
}
518508
#endif
@@ -849,6 +839,131 @@ class thread_safe_trx_ilist_t
849839
alignas(CPU_LEVEL1_DCACHE_LINESIZE) ilist<trx_t> trx_list;
850840
};
851841

842+
/**
843+
Active read-write transaction identifiers and serialisation numbers container.
844+
845+
Unlike rw_trx_hash_t, which is optimized for direct lookup, this
846+
structure is optimized for compact storage and traversal of active
847+
transactions by MVCC read view construction.
848+
849+
The vector may contain empty slots corresponding to idle or read-only
850+
transactions that currently do not own an active read-write trx_id.
851+
Such slots are skipped during traversal.
852+
*/
853+
class rw_trx_vector
854+
{
855+
struct rw_trx_id
856+
{
857+
Atomic_relaxed<trx_id_t> id{TRX_ID_MAX};
858+
Atomic_relaxed<trx_id_t> no{TRX_ID_MAX};
859+
};
860+
alignas(CPU_LEVEL1_DCACHE_LINESIZE)
861+
std::vector<rw_trx_id, ut_allocator<rw_trx_id>>
862+
ids{ut_allocator<rw_trx_id>(mem_key_trx_sys_t_rw_trx_ids)};
863+
alignas(CPU_LEVEL1_DCACHE_LINESIZE)
864+
std::vector<trx_t*, ut_allocator<trx_t*>>
865+
trxs{ut_allocator<trx_t*>(mem_key_trx_sys_t_rw_trx_ids)};
866+
alignas(CPU_LEVEL1_DCACHE_LINESIZE) mutable srw_spin_lock_low latch;
867+
868+
public:
869+
template <typename Functor1, typename Functor2>
870+
trx_id_t assign_new_trx_no(const trx_t *trx, Functor1 get_new_trx_id,
871+
Functor2 refresh_rw_trx_hash_version) noexcept
872+
{
873+
trx_id_t no;
874+
latch.rd_lock();
875+
ut_ad(trx->rw_trx_ids_slot < ids.size());
876+
ut_ad(trxs[trx->rw_trx_ids_slot] == trx);
877+
ut_ad(ids[trx->rw_trx_ids_slot].id == trx->id);
878+
ut_ad(ids[trx->rw_trx_ids_slot].no == TRX_ID_MAX);
879+
ids[trx->rw_trx_ids_slot].no= no= get_new_trx_id();
880+
refresh_rw_trx_hash_version();
881+
latch.rd_unlock();
882+
return no;
883+
}
884+
trx_id_t snapshot_ids(trx_ids_t &view_ids,
885+
const trx_id_t max_trx_id) const noexcept
886+
{
887+
trx_id_t min_trx_no{max_trx_id};
888+
view_ids.clear();
889+
latch.rd_lock();
890+
view_ids.reserve(ids.size());
891+
for (const auto &it : ids)
892+
{
893+
trx_id_t id{it.id};
894+
if (id < max_trx_id)
895+
{
896+
view_ids.push_back(id);
897+
const trx_id_t no{it.no};
898+
if (no < min_trx_no)
899+
min_trx_no= no;
900+
}
901+
}
902+
latch.rd_unlock();
903+
return min_trx_no;
904+
}
905+
template <typename Functor1, typename Functor2>
906+
void register_rw(const trx_t *trx, Functor1 get_new_trx_id,
907+
Functor2 refresh_rw_trx_hash_version) noexcept
908+
{
909+
latch.rd_lock();
910+
ut_ad(trx->rw_trx_ids_slot < ids.size());
911+
ut_ad(trxs[trx->rw_trx_ids_slot] == trx);
912+
ut_ad(ids[trx->rw_trx_ids_slot].id == TRX_ID_MAX);
913+
ut_ad(ids[trx->rw_trx_ids_slot].no == TRX_ID_MAX);
914+
ids[trx->rw_trx_ids_slot].id= get_new_trx_id();
915+
refresh_rw_trx_hash_version();
916+
latch.rd_unlock();
917+
}
918+
void deregister_rw(const trx_t *trx) noexcept
919+
{
920+
latch.rd_lock();
921+
ut_ad(trx->rw_trx_ids_slot < ids.size());
922+
rw_trx_id &slot= ids[trx->rw_trx_ids_slot];
923+
ut_ad(trxs[trx->rw_trx_ids_slot] == trx);
924+
ut_ad(slot.id == trx->id);
925+
slot.id= TRX_ID_MAX;
926+
slot.no= TRX_ID_MAX;
927+
latch.rd_unlock();
928+
}
929+
void register_trx(trx_t *trx) noexcept
930+
{
931+
ut_ad(trx->rw_trx_ids_slot == std::numeric_limits<uint32_t>::max());
932+
latch.wr_lock();
933+
trx->rw_trx_ids_slot= static_cast<uint32_t>(ids.size());
934+
ids.emplace_back();
935+
trxs.emplace_back(trx);
936+
latch.wr_unlock();
937+
}
938+
void deregister_trx(trx_t *trx) noexcept
939+
{
940+
latch.wr_lock();
941+
ut_ad(trx->rw_trx_ids_slot < ids.size());
942+
ut_ad(trxs[trx->rw_trx_ids_slot] == trx);
943+
if (trx->rw_trx_ids_slot + 1 < ids.size())
944+
{
945+
trx_t *move_trx= trxs.back();
946+
ids[trx->rw_trx_ids_slot]= std::move(ids.back());
947+
trxs[trx->rw_trx_ids_slot]= std::move(trxs.back());
948+
move_trx->rw_trx_ids_slot= trx->rw_trx_ids_slot;
949+
}
950+
ids.pop_back();
951+
trxs.pop_back();
952+
latch.wr_unlock();
953+
trx->rw_trx_ids_slot= std::numeric_limits<uint32_t>::max();
954+
}
955+
void create() noexcept
956+
{
957+
ut_ad(ids.size() == 0);
958+
latch.init();
959+
}
960+
void destroy() noexcept
961+
{
962+
ut_ad(ids.size() == 0);
963+
latch.destroy();
964+
}
965+
};
966+
852967
/** The transaction system central memory data structure. */
853968
class trx_sys_t
854969
{
@@ -875,6 +990,15 @@ class trx_sys_t
875990

876991
/** False if there is no undo log to purge or rollback */
877992
bool undo_log_nonempty;
993+
994+
/**
995+
Collection of active read-write transaction identifiers and serialization
996+
numbers used for MVCC snapshot creation.
997+
998+
This complements rw_trx_hash with a traversal-friendly representation
999+
optimized for collecting active transaction ids.
1000+
*/
1001+
rw_trx_vector rw_trx_ids;
8781002
public:
8791003
/** List of all transactions. */
8801004
thread_safe_trx_ilist_t trx_list;
@@ -1014,7 +1138,7 @@ class trx_sys_t
10141138
next call to trx_sys.get_new_trx_id()
10151139
*/
10161140

1017-
trx_id_t get_max_trx_id()
1141+
trx_id_t get_max_trx_id() const noexcept
10181142
{
10191143
return m_max_trx_id;
10201144
}
@@ -1037,7 +1161,7 @@ class trx_sys_t
10371161
Allocates and assigns new transaction serialisation number.
10381162
10391163
There's a gap between m_max_trx_id increment and transaction serialisation
1040-
number becoming visible through rw_trx_hash. While we're in this gap
1164+
number becoming visible through rw_trx_ids. While we're in this gap
10411165
concurrent thread may come and do MVCC snapshot without seeing allocated
10421166
but not yet assigned serialisation number. Then at some point purge thread
10431167
may clone this view. As a result it won't see newly allocated serialisation
@@ -1047,58 +1171,43 @@ class trx_sys_t
10471171
m_rw_trx_hash_version is intended to solve this problem. MVCC snapshot has
10481172
to wait until m_max_trx_id == m_rw_trx_hash_version, which effectively
10491173
means that all transaction serialisation numbers up to m_max_trx_id are
1050-
available through rw_trx_hash.
1174+
available through rw_trx_ids.
10511175
10521176
We rely on refresh_rw_trx_hash_version() to issue RELEASE memory barrier so
1053-
that m_rw_trx_hash_version increment happens after
1054-
trx->rw_trx_hash_element->no becomes visible through rw_trx_hash.
1177+
that m_rw_trx_hash_version increment happens after transaction serialisation
1178+
number becomes visible through rw_trx_ids.
10551179
10561180
@param trx transaction
10571181
*/
1058-
void assign_new_trx_no(trx_t *trx)
1182+
trx_id_t assign_new_trx_no(trx_t *trx)
10591183
{
1060-
trx->rw_trx_hash_element->no= get_new_trx_id_no_refresh();
1061-
refresh_rw_trx_hash_version();
1184+
return rw_trx_ids.assign_new_trx_no(trx,
1185+
[this](){ return get_new_trx_id_no_refresh(); },
1186+
[this](){ refresh_rw_trx_hash_version(); });
10621187
}
10631188

10641189

10651190
/**
10661191
Takes MVCC snapshot.
10671192
1068-
To reduce malloc probability we reserve rw_trx_hash.size() + 32 elements
1069-
in ids.
1070-
10711193
For details about get_rw_trx_hash_version() != get_max_trx_id() spin
10721194
@sa register_rw() and @sa assign_new_trx_no().
10731195
10741196
We rely on get_rw_trx_hash_version() to issue ACQUIRE memory barrier so
1075-
that loading of m_rw_trx_hash_version happens before accessing rw_trx_hash.
1197+
that loading of m_rw_trx_hash_version happens before accessing rw_trx_ids.
10761198
1077-
To optimise snapshot creation rw_trx_hash.iterate() is being used instead
1078-
of rw_trx_hash.iterate_no_dups(). It means that some transaction
1079-
identifiers may appear multiple times in ids.
1080-
1081-
@param[in,out] caller_trx used to get access to rw_trx_hash_pins
10821199
@param[out] ids array to store registered transaction identifiers
10831200
@param[out] max_trx_id variable to store m_max_trx_id value
1084-
@param[out] mix_trx_no variable to store min(no) value
1201+
1202+
@return min(no)
10851203
*/
10861204

1087-
void snapshot_ids(trx_t *caller_trx, trx_ids_t *ids, trx_id_t *max_trx_id,
1088-
trx_id_t *min_trx_no)
1205+
trx_id_t snapshot_ids(trx_ids_t &ids, trx_id_t &max_trx_id) const noexcept
10891206
{
1090-
snapshot_ids_arg arg(ids);
1091-
1092-
while ((arg.m_id= get_rw_trx_hash_version()) != get_max_trx_id())
1207+
while ((max_trx_id= get_rw_trx_hash_version()) != get_max_trx_id())
10931208
ut_delay(1);
1094-
arg.m_no= arg.m_id;
1095-
1096-
ids->clear();
1097-
ids->reserve(rw_trx_hash.size() + 32);
1098-
rw_trx_hash.iterate(caller_trx, copy_one_id, &arg);
10991209

1100-
*max_trx_id= arg.m_id;
1101-
*min_trx_no= arg.m_no;
1210+
return rw_trx_ids.snapshot_ids(ids, max_trx_id);
11021211
}
11031212

11041213

@@ -1149,7 +1258,7 @@ class trx_sys_t
11491258
Transaction becomes visible to MVCC.
11501259
11511260
There's a gap between m_max_trx_id increment and transaction becoming
1152-
visible through rw_trx_hash. While we're in this gap concurrent thread may
1261+
visible through rw_trx_ids. While we're in this gap concurrent thread may
11531262
come and do MVCC snapshot. As a result concurrent read view will be able to
11541263
observe records owned by this transaction even before it was committed.
11551264
@@ -1165,21 +1274,32 @@ class trx_sys_t
11651274

11661275
void register_rw(trx_t *trx)
11671276
{
1168-
trx->id= get_new_trx_id_no_refresh();
1277+
rw_trx_ids.register_rw(trx,
1278+
[this, trx](){ return trx->id= get_new_trx_id_no_refresh(); },
1279+
[this](){ refresh_rw_trx_hash_version(); });
11691280
rw_trx_hash.insert(trx);
1170-
refresh_rw_trx_hash_version();
1281+
}
1282+
1283+
1284+
void resurrect_rw(trx_t *trx)
1285+
{
1286+
rw_trx_ids.register_rw(trx, [trx](){ return trx->id; }, [](){});
1287+
rw_trx_hash.insert(trx);
1288+
rw_trx_hash.put_pins(trx);
11711289
}
11721290

11731291

11741292
/**
11751293
Deregisters read-write transaction.
11761294
1177-
Transaction is removed from rw_trx_hash, which releases all implicit locks.
1178-
MVCC snapshot won't see this transaction anymore.
1295+
After this call the transaction is no longer visible as active to MVCC read
1296+
views created subsequently, and all implicit locks held by the transaction
1297+
have been released.
11791298
*/
11801299

1181-
void deregister_rw(trx_t *trx)
1300+
void deregister_rw(trx_t *trx) noexcept
11821301
{
1302+
rw_trx_ids.deregister_rw(trx);
11831303
rw_trx_hash.erase(trx);
11841304
}
11851305

@@ -1204,6 +1324,7 @@ class trx_sys_t
12041324
void register_trx(trx_t *trx)
12051325
{
12061326
trx_list.push_front(*trx);
1327+
rw_trx_ids.register_trx(trx);
12071328
}
12081329

12091330

@@ -1214,6 +1335,7 @@ class trx_sys_t
12141335
*/
12151336
void deregister_trx(trx_t *trx)
12161337
{
1338+
rw_trx_ids.deregister_trx(trx);
12171339
trx_list.remove(*trx);
12181340
}
12191341

@@ -1266,33 +1388,8 @@ class trx_sys_t
12661388
private:
12671389
static my_bool find_same_or_older_callback(void *el, void *i) noexcept;
12681390

1269-
1270-
struct snapshot_ids_arg
1271-
{
1272-
snapshot_ids_arg(trx_ids_t *ids): m_ids(ids) {}
1273-
trx_ids_t *m_ids;
1274-
trx_id_t m_id;
1275-
trx_id_t m_no;
1276-
};
1277-
1278-
1279-
static my_bool copy_one_id(void* el, void *a)
1280-
{
1281-
auto element= static_cast<const rw_trx_hash_element_t *>(el);
1282-
auto arg= static_cast<snapshot_ids_arg*>(a);
1283-
if (element->id < arg->m_id)
1284-
{
1285-
trx_id_t no= element->no;
1286-
arg->m_ids->push_back(element->id);
1287-
if (no < arg->m_no)
1288-
arg->m_no= no;
1289-
}
1290-
return 0;
1291-
}
1292-
1293-
12941391
/** Getter for m_rw_trx_hash_version, must issue ACQUIRE memory barrier. */
1295-
trx_id_t get_rw_trx_hash_version()
1392+
trx_id_t get_rw_trx_hash_version() const noexcept
12961393
{
12971394
return m_rw_trx_hash_version.load(std::memory_order_acquire);
12981395
}

storage/innobase/include/trx0trx.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,8 @@ struct trx_t : ilist_node<>
629629

630630

631631
public:
632+
/** trx_sys.rw_trx_ids index, protected by trx_sys.rw_trx_ids.latch */
633+
uint32_t rw_trx_ids_slot;
632634
/** Transaction identifier (0 if no locks were acquired).
633635
Set by trx_sys_t::register_rw() or trx_resurrect() before
634636
the transaction is added to trx_sys.rw_trx_hash.

storage/innobase/include/ut0new.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ extern PSI_memory_key mem_key_other;
174174
extern PSI_memory_key mem_key_row_log_buf;
175175
extern PSI_memory_key mem_key_row_merge_sort;
176176
extern PSI_memory_key mem_key_std;
177+
extern PSI_memory_key mem_key_trx_sys_t_rw_trx_ids;
177178

178179
/** Setup the internal objects needed for UT_NEW() to operate.
179180
This must be called before the first call to UT_NEW(). */

0 commit comments

Comments
 (0)