11#pragma once
22
33#include < algorithm>
4+ #include < array>
45#include < atomic>
56#include < chrono>
67#include < cmath>
78#include < cstddef>
89#include < cstdint>
10+ #include < cstring>
911#include < map>
1012#include < mutex>
1113#include < numeric>
14+ #include < span>
1215#include < thread>
1316#include < utility>
1417#include < vector>
@@ -626,6 +629,34 @@ runCoherenceProbe(const std::vector<std::uint32_t> &cpus,
626629 return out;
627630}
628631
632+ // / Mutex plus map backing the process-wide coherence-probe cache, keyed on
633+ // / the sorted-unique `cpus` vector. A single owner so `cachedCoherenceProbe`
634+ // / (lookup then insert) and `seedCoherenceProbeCache` (external insert) share
635+ // / one map instead of two function-local statics.
636+ struct CoherenceProbeCache {
637+ // / Guards every read and write of `entries`.
638+ std::mutex mutex;
639+ // / Cached probe per normalised cpuset key.
640+ std::map<std::vector<std::uint32_t >, CoherenceProbe> entries;
641+ };
642+
643+ // / Accessor for the single process-wide `CoherenceProbeCache`.
644+ inline CoherenceProbeCache &coherenceProbeCache () noexcept {
645+ static CoherenceProbeCache cache;
646+ return cache;
647+ }
648+
649+ // / Normalise a CPU list into the cache key: sorted and deduplicated so a
650+ // / pool's `probeCpus` and a seeded probe's embedded `cpus` map to the same
651+ // / entry regardless of input order.
652+ inline std::vector<std::uint32_t >
653+ coherenceProbeCacheKey (const std::vector<std::uint32_t > &cpus) {
654+ std::vector<std::uint32_t > key = cpus;
655+ std::sort (key.begin (), key.end ());
656+ key.erase (std::unique (key.begin (), key.end ()), key.end ());
657+ return key;
658+ }
659+
629660// / Process-wide cache for `runCoherenceProbe`, keyed on the
630661// / sorted-unique `cpus` vector. The latency matrix depends only on the
631662// / host hardware, so repeated probes for the same set are duplicate
@@ -635,17 +666,13 @@ inline CoherenceProbe
635666cachedCoherenceProbe (const std::vector<std::uint32_t > &cpus,
636667 const std::vector<std::vector<std::uint32_t >> &sysfsPrior,
637668 std::uint32_t roundTrips = 1024U ) {
638- std::vector<std::uint32_t > key = cpus;
639- std::sort (key.begin (), key.end ());
640- key.erase (std::unique (key.begin (), key.end ()), key.end ());
641-
642- static std::mutex cacheMutex;
643- static std::map<std::vector<std::uint32_t >, CoherenceProbe> cache;
669+ std::vector<std::uint32_t > key = coherenceProbeCacheKey (cpus);
670+ CoherenceProbeCache &cache = coherenceProbeCache ();
644671
645672 {
646- const std::scoped_lock guard (cacheMutex );
647- const auto hit = cache.find (key);
648- if (hit != cache.end ()) {
673+ const std::scoped_lock guard (cache. mutex );
674+ const auto hit = cache.entries . find (key);
675+ if (hit != cache.entries . end ()) {
649676 return hit->second ;
650677 }
651678 }
@@ -656,8 +683,263 @@ cachedCoherenceProbe(const std::vector<std::uint32_t> &cpus,
656683 // first inserter's copy so identical pools see identical numbers.
657684 CoherenceProbe fresh = runCoherenceProbe (cpus, sysfsPrior, roundTrips);
658685
659- const std::scoped_lock guard (cacheMutex);
660- return cache.emplace (std::move (key), std::move (fresh)).first ->second ;
686+ const std::scoped_lock guard (cache.mutex );
687+ return cache.entries .emplace (std::move (key), std::move (fresh)).first ->second ;
688+ }
689+
690+ // / Insert `probe` into the process-wide cache under the normalised `cpus`
691+ // / key so the next pool whose worker cpuset matches that key skips the live
692+ // / probe. Replaces any existing entry for the key: a seed is an explicit
693+ // / caller decision to use this probe for that cpuset, and import is meant to
694+ // / run before the first pool is built, where the cache is empty.
695+ inline void seedCoherenceProbeCache (const std::vector<std::uint32_t > &cpus,
696+ const CoherenceProbe &probe) {
697+ std::vector<std::uint32_t > key = coherenceProbeCacheKey (cpus);
698+ CoherenceProbeCache &cache = coherenceProbeCache ();
699+ const std::scoped_lock guard (cache.mutex );
700+ cache.entries .insert_or_assign (std::move (key), probe);
701+ }
702+
703+ // / Magic prefix on the self-describing probe blob. Spells 'COHP'. A blob
704+ // / whose first word is not this magic is rejected by `importCoherenceProbe`.
705+ inline constexpr std::uint32_t kCoherenceProbeMagic = 0x434F4850U ;
706+ // / Blob layout version. Bumped when the field order or encoding changes so a
707+ // / blob from an incompatible citor is rejected rather than misread.
708+ inline constexpr std::uint32_t kCoherenceProbeFormatVersion = 1U ;
709+
710+ // / Append the raw bytes of a trivially-copyable scalar to `bytes` in native
711+ // / byte order. The cache is same-machine, so native endianness is fine and
712+ // / the magic plus version guard against cross-build corruption.
713+ template <typename T>
714+ inline void coherenceProbeWritePod (std::vector<std::byte> &bytes,
715+ const T &value) {
716+ std::array<std::byte, sizeof (T)> buf{};
717+ std::memcpy (buf.data (), &value, sizeof (T));
718+ bytes.insert (bytes.end (), buf.begin (), buf.end ());
719+ }
720+
721+ // / Write a `std::uint32_t` vector as a `std::uint64_t` length prefix followed
722+ // / by the elements.
723+ inline void coherenceProbeWriteU32Vector (std::vector<std::byte> &bytes,
724+ const std::vector<std::uint32_t > &v) {
725+ coherenceProbeWritePod (bytes, static_cast <std::uint64_t >(v.size ()));
726+ for (const std::uint32_t x : v) {
727+ coherenceProbeWritePod (bytes, x);
728+ }
729+ }
730+
731+ // / Write a `double` vector as a `std::uint64_t` length prefix followed by the
732+ // / elements.
733+ inline void coherenceProbeWriteF64Vector (std::vector<std::byte> &bytes,
734+ const std::vector<double > &v) {
735+ coherenceProbeWritePod (bytes, static_cast <std::uint64_t >(v.size ()));
736+ for (const double x : v) {
737+ coherenceProbeWritePod (bytes, x);
738+ }
739+ }
740+
741+ // / Write a row-major matrix as a row count followed by each row through
742+ // / `coherenceProbeWriteF64Vector`.
743+ inline void
744+ coherenceProbeWriteF64Matrix (std::vector<std::byte> &bytes,
745+ const std::vector<std::vector<double >> &m) {
746+ coherenceProbeWritePod (bytes, static_cast <std::uint64_t >(m.size ()));
747+ for (const auto &row : m) {
748+ coherenceProbeWriteF64Vector (bytes, row);
749+ }
750+ }
751+
752+ // / Serialise a `CoherenceProbe` to a self-describing binary blob: magic,
753+ // / format version, then every field with length-prefixed vectors and
754+ // / native-endian doubles.
755+ inline std::vector<std::byte>
756+ serializeCoherenceProbe (const CoherenceProbe &probe) {
757+ std::vector<std::byte> bytes;
758+ coherenceProbeWritePod (bytes, kCoherenceProbeMagic );
759+ coherenceProbeWritePod (bytes, kCoherenceProbeFormatVersion );
760+ coherenceProbeWritePod (bytes,
761+ static_cast <std::uint8_t >(probe.valid ? 1U : 0U ));
762+ coherenceProbeWritePod (bytes, probe.maxCrossOverIntraRatio );
763+ coherenceProbeWritePod (
764+ bytes, static_cast <std::uint8_t >(probe.matrix .valid ? 1U : 0U ));
765+ coherenceProbeWriteU32Vector (bytes, probe.matrix .cpus );
766+ coherenceProbeWriteF64Matrix (bytes, probe.matrix .matrix );
767+ coherenceProbeWriteU32Vector (bytes, probe.clusters .clusterIdOfCpuIndex );
768+ coherenceProbeWritePod (bytes, probe.clusters .numClusters );
769+ coherenceProbeWriteF64Matrix (bytes, probe.clusters .clusterDistanceNs );
770+ return bytes;
771+ }
772+
773+ // / Bounds-checked cursor over a byte blob. Every read first confirms the
774+ // / blob holds enough bytes; a short read latches `ok` to false and leaves
775+ // / the output untouched so a malformed blob can never read out of range.
776+ struct CoherenceProbeReader {
777+ // / Next byte to read.
778+ const std::byte *cur = nullptr ;
779+ // / One past the last readable byte.
780+ const std::byte *end = nullptr ;
781+ // / False once any read ran short; latches and is never cleared.
782+ bool ok = true ;
783+
784+ // / Bytes left between `cur` and `end`.
785+ [[nodiscard]] std::size_t remainingBytes () const noexcept {
786+ return static_cast <std::size_t >(end - cur);
787+ }
788+
789+ // / Copy one `T` out of the blob and advance. Returns false and latches
790+ // / `ok` when fewer than `sizeof(T)` bytes remain; `out` is left untouched.
791+ template <typename T>
792+ bool readPod (T &out) noexcept {
793+ if (!ok || remainingBytes () < sizeof (T)) {
794+ ok = false ;
795+ return false ;
796+ }
797+ std::memcpy (&out, cur, sizeof (T));
798+ cur += sizeof (T);
799+ return true ;
800+ }
801+ };
802+
803+ // / Read a length-prefixed `std::uint32_t` vector. Rejects a length that
804+ // / exceeds the remaining bytes before resizing, so a hostile prefix cannot
805+ // / force an oversized allocation.
806+ inline bool coherenceProbeReadU32Vector (CoherenceProbeReader &r,
807+ std::vector<std::uint32_t > &out) {
808+ std::uint64_t count = 0 ;
809+ if (!r.readPod (count)) {
810+ return false ;
811+ }
812+ // Divide before multiply so a hostile count cannot overflow the size
813+ // check; each element occupies a fixed four bytes.
814+ if (count > r.remainingBytes () / sizeof (std::uint32_t )) {
815+ r.ok = false ;
816+ return false ;
817+ }
818+ out.resize (static_cast <std::size_t >(count));
819+ for (std::uint32_t &x : out) {
820+ if (!r.readPod (x)) {
821+ return false ;
822+ }
823+ }
824+ return true ;
825+ }
826+
827+ // / Read a length-prefixed `double` vector, with the same oversized-length
828+ // / guard as `coherenceProbeReadU32Vector`.
829+ inline bool coherenceProbeReadF64Vector (CoherenceProbeReader &r,
830+ std::vector<double > &out) {
831+ std::uint64_t count = 0 ;
832+ if (!r.readPod (count)) {
833+ return false ;
834+ }
835+ if (count > r.remainingBytes () / sizeof (double )) {
836+ r.ok = false ;
837+ return false ;
838+ }
839+ out.resize (static_cast <std::size_t >(count));
840+ for (double &x : out) {
841+ if (!r.readPod (x)) {
842+ return false ;
843+ }
844+ }
845+ return true ;
846+ }
847+
848+ // / Read a row count followed by that many `double` rows through
849+ // / `coherenceProbeReadF64Vector`. Bounds the row count by the remaining
850+ // / bytes since every row carries at least its own length prefix.
851+ inline bool coherenceProbeReadF64Matrix (CoherenceProbeReader &r,
852+ std::vector<std::vector<double >> &out) {
853+ std::uint64_t rows = 0 ;
854+ if (!r.readPod (rows)) {
855+ return false ;
856+ }
857+ // Each row carries at least its own eight-byte length prefix, so the row
858+ // count cannot exceed the remaining bytes divided by that minimum.
859+ if (rows > r.remainingBytes () / sizeof (std::uint64_t )) {
860+ r.ok = false ;
861+ return false ;
862+ }
863+ out.resize (static_cast <std::size_t >(rows));
864+ for (auto &row : out) {
865+ if (!coherenceProbeReadF64Vector (r, row)) {
866+ return false ;
867+ }
868+ }
869+ return true ;
870+ }
871+
872+ // / Parse a blob produced by `serializeCoherenceProbe` into `out`. Returns
873+ // / false on magic or version mismatch, truncation, or a structural
874+ // / inconsistency (latency matrix not NxN over the embedded cpus, cluster
875+ // / fields not sized to `numClusters`). `out` is written only on success.
876+ inline bool deserializeCoherenceProbe (std::span<const std::byte> bytes,
877+ CoherenceProbe &out) {
878+ CoherenceProbeReader r{
879+ .cur = bytes.data (), .end = bytes.data () + bytes.size (), .ok = true };
880+
881+ std::uint32_t magic = 0 ;
882+ std::uint32_t version = 0 ;
883+ if (!r.readPod (magic) || !r.readPod (version)) {
884+ return false ;
885+ }
886+ if (magic != kCoherenceProbeMagic ||
887+ version != kCoherenceProbeFormatVersion ) {
888+ return false ;
889+ }
890+
891+ CoherenceProbe probe;
892+ std::uint8_t probeValid = 0 ;
893+ if (!r.readPod (probeValid)) {
894+ return false ;
895+ }
896+ probe.valid = probeValid != 0U ;
897+ if (!r.readPod (probe.maxCrossOverIntraRatio )) {
898+ return false ;
899+ }
900+ std::uint8_t matrixValid = 0 ;
901+ if (!r.readPod (matrixValid)) {
902+ return false ;
903+ }
904+ probe.matrix .valid = matrixValid != 0U ;
905+ if (!coherenceProbeReadU32Vector (r, probe.matrix .cpus ) ||
906+ !coherenceProbeReadF64Matrix (r, probe.matrix .matrix ) ||
907+ !coherenceProbeReadU32Vector (r, probe.clusters .clusterIdOfCpuIndex ) ||
908+ !r.readPod (probe.clusters .numClusters ) ||
909+ !coherenceProbeReadF64Matrix (r, probe.clusters .clusterDistanceNs )) {
910+ return false ;
911+ }
912+ if (!r.ok ) {
913+ return false ;
914+ }
915+
916+ // Structural consistency: the matrix must be NxN over the embedded cpu
917+ // list, the per-cpu cluster ids must cover every cpu, and the cluster
918+ // distance matrix must be numClusters x numClusters.
919+ const std::size_t n = probe.matrix .cpus .size ();
920+ if (probe.matrix .matrix .size () != n) {
921+ return false ;
922+ }
923+ for (const auto &row : probe.matrix .matrix ) {
924+ if (row.size () != n) {
925+ return false ;
926+ }
927+ }
928+ if (probe.clusters .clusterIdOfCpuIndex .size () != n) {
929+ return false ;
930+ }
931+ const std::size_t k = probe.clusters .numClusters ;
932+ if (probe.clusters .clusterDistanceNs .size () != k) {
933+ return false ;
934+ }
935+ for (const auto &row : probe.clusters .clusterDistanceNs ) {
936+ if (row.size () != k) {
937+ return false ;
938+ }
939+ }
940+
941+ out = std::move (probe);
942+ return true ;
661943}
662944
663945} // namespace citor::detail
0 commit comments