Source File
typedadj.go
Belonging Package
github.com/freeeve/gochickpeas
// Typed adjacency: a lazily built per-(type, direction) CSR view --// offsets, neighbors, and original CSR positions restricted to one// relationship type -- so a single-type traversal reads a contiguous run// with no per-relationship type test. Snapshot.Match resolves the view// holder once per compiled stage (one lock there, none per traversal// call); each direction builds on first traversal via its own Once.// Per-node relative order matches the primary CSR, so routed results are// byte-identical to the scan path. Types below the memory floor never// build the id-space offsets array (it would dwarf their payload); they// route through a payload-proportional run view instead (typedRuns), with// the type-tested scan remaining the multi-type / MatchAll path.package chickpeasimport ()// typedCSR is one direction's single-type view. nil means "not built"// (below the floor): callers fall back to the type-tested scan.type typedCSR struct {offsets []uint32nbrs []NodeIDposs []uint32}// typedRuns is one direction's below-floor single-type view: the type's// relationships filtered from the primary CSR in original order, with the// owning node per entry instead of id-space offsets -- memory proportional// to the type's payload. A node's run resolves through the bucket hint: a// binary search bounded to the node's 64-id bucket, so the probes land in// the one or two cache lines the bucket's entries span instead of walking// ~log2(E) cold lines across the whole owner array. Per-node relative// order matches the primary CSR, so routed results are byte-identical to// the scan path.type typedRuns struct {nodes []uint32nbrs []NodeIDposs []uint32// hint[b] is the index in nodes of the first entry whose owner falls in// bucket b (owners b<<runHintShift and up); hint[b+1] closes the bucket.// One uint32 per 64 id-space slots -- 1/64th of the full offsets array// the typed floor exists to avoid -- built read-only with the view, so// parallel kernels share it without a single cross-core write.hint []uint32}// runHintShift buckets the owner array by node id for the run-range// lookup: 64 ids per bucket keeps the hint at one uint32 per 64 id-space// slots while bounding each lookup's search to the entries of 64// consecutive nodes of a below-floor type -- typically a handful, always// contiguous. The known alternative is a FULL per-id starts array --// strictly O(1) runRange at ~64x the memory per consulted direction// (id-space proportional, the very array the typed floor avoids); since// these views already build lazily per consulted (type, direction), that// trade is one constant swap away if a CPU profile ever shows the// bucketed search hot on a hub-heavy workload.const runHintShift = 6// runRange is node's [lo, hi) span in the run view: the hint bounds the// bucket, a binary search inside it finds the run start.func ( *typedRuns) ( NodeID) (int, int) {:= int() >> runHintShiftif +1 >= len(.hint) {return 0, 0}, := int(.hint[]), int(.hint[+1]), := slices.BinarySearch(.nodes[:], uint32())+=:=for < && .nodes[] == uint32() {++}return ,}// typedPair lazily holds both directions' views for one type, plus the// type's sorted edge-key set and below-floor run views -- representations// sized by the type's payload instead of the id space.type typedPair struct {g *Snapshott RelTypeoutOnce sync.OnceinOnce sync.Onceout, in *typedCSRedgeOnce sync.Onceedges []uint64outRunsOnce sync.OnceinRunsOnce sync.OnceoutRuns, inRuns *typedRuns}// typedFloor: a type builds its view only when its relationship count is// at least CSRIDSpace/4, keeping the mandatory offsets array (one uint32// per id-space slot) proportional to the payload it accelerates.func ( *Snapshot) ( RelType) bool {, := .typeIndex[]if ! {return false}return .Len()*4 >= int(.CSRIDSpace())}// view returns the direction's typed CSR, building it on first use; nil// when the type sits below the floor.func ( *typedPair) ( bool) *typedCSR {if == nil {return nil}if {.outOnce.Do(func() {if .g.typedAboveFloor(.t) {.out = buildTypedCSR(.g.outOffsets, .g.outNbrs, .g.outTypes, nil, .t)}})return .out}.inOnce.Do(func() {if .g.typedAboveFloor(.t) {// The incoming view bakes the inToOut position mapping in, so// property reads index the stored (outgoing) positions directly;// an absent mapping keeps raw indexes, mirroring relsYield..in = buildTypedCSR(.g.inOffsets, .g.inNbrs, .g.inTypes, .g.getInToOut(), .t)}})return .in}// runs returns the direction's below-floor run view, building it on first// use; nil when the type has a full typed CSR instead (at or above the// floor, where view() serves the traversal). Together the two views cover// every single-type traversal: contiguous per-type CSR above the floor,// payload-proportional filtered runs below it.func ( *typedPair) ( bool) *typedRuns {if == nil {return nil}if {.outRunsOnce.Do(func() {if !.g.typedAboveFloor(.t) {.outRuns = buildTypedRuns(.g.outOffsets, .g.outNbrs, .g.outTypes, nil, .t, .typeCount())}})return .outRuns}.inRunsOnce.Do(func() {if !.g.typedAboveFloor(.t) {.inRuns = buildTypedRuns(.g.inOffsets, .g.inNbrs, .g.inTypes, .g.getInToOut(), .t, .typeCount())}})return .inRuns}// typeCount is the type's relationship count (0 for an unknown type).func ( *typedPair) () int {if , := .g.typeIndex[.t]; {return .Len()}return 0}// buildTypedRuns filters one direction's CSR to a single type in one// linear pass, keeping the owning node per entry, then indexes the owner// array with the bucket hint. poss carries each kept relationship's// property-read position, mapped like buildTypedCSR's.func buildTypedRuns( []uint32, []NodeID, []RelType, []uint32, RelType, int) *typedRuns {:= &typedRuns{nodes: make([]uint32, 0, ),nbrs: make([]NodeID, 0, ),poss: make([]uint32, 0, ),}:= len() - 1if < 0 {= 0}for := 0; < ; ++ {for := int([]); < int([+1]); ++ {if [] == {:= uint32()if < len() {= []}.nodes = append(.nodes, uint32()).nbrs = append(.nbrs, []).poss = append(.poss, )}}}.hint = make([]uint32, (>>runHintShift)+2):= 0for , := range .nodes {for <= int()>>runHintShift {.hint[] = uint32()++}}for ; < len(.hint); ++ {.hint[] = uint32(len(.nodes))}return}// runScanFloor gates the run view per node: at or below this primary-CSR// degree the type-tested scan reads the node's whole mixed run in a cache// line or two, beating the view's lookup; above it the filtered contiguous// run wins. Callers check the degree before consulting runs().const runScanFloor = 64// typedSlotsLen is the dense-id fast path's span: rel-type atoms below it// resolve through a per-slot atomic slice (two atomic loads, no hashing);// the rare larger atom falls back to the copy-on-write map. Both paths// are lock-free on the hit, since the string-typed traversal conveniences// resolve per call inside kernel loops.const typedSlotsLen = 4096// typedPairFor returns the shared lazy holder for one type. Holders are// created at most once per type (CAS / mutexed copy-insert) and never// replaced, so a stale read only re-misses.func ( *Snapshot) ( RelType) *typedPair {if < typedSlotsLen {:= .typedSlots.Load()if == nil {.typedMu.Lock()if = .typedSlots.Load(); == nil {= &[typedSlotsLen]atomic.Pointer[typedPair]{}.typedSlots.Store()}.typedMu.Unlock()}if := [].Load(); != nil {return}:= &typedPair{g: , t: }if ![].CompareAndSwap(nil, ) {= [].Load()}return}if := .typedAdj.Load(); != nil {if , := (*)[]; {return}}.typedMu.Lock()defer .typedMu.Unlock():= .typedAdj.Load()if != nil {if , := (*)[]; {return}}:= make(map[RelType]*typedPair, 1)if != nil {maps.Copy(, *)}:= &typedPair{g: , t: }[] =.typedAdj.Store(&)return}// buildTypedCSR restricts one direction's CSR to a single type in one// linear pass, preserving per-node relative order. poss carries each kept// relationship's property-read position: the raw index, or posMap[index]// when a mapping is supplied (the incoming direction's inToOut).func buildTypedCSR( []uint32, []NodeID, []RelType, []uint32, RelType) *typedCSR {:= len() - 1if < 0 {return &typedCSR{offsets: []uint32{0}}}:= 0for , := range {if == {++}}:= &typedCSR{offsets: make([]uint32, +1),nbrs: make([]NodeID, 0, ),poss: make([]uint32, 0, ),}for := 0; < ; ++ {.offsets[] = uint32(len(.nbrs))for := int([]); < int([+1]); ++ {if [] == {:= uint32()if < len() {= []}.nbrs = append(.nbrs, []).poss = append(.poss, )}}}.offsets[] = uint32(len(.nbrs))return}
![]() |
The pages are generated with Golds v0.8.7. (GOOS=linux GOARCH=amd64) Golds is a Go 101 project developed by Tapir Liu. PR and bug reports are welcome and can be submitted to the issue list. Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds. |