Well, therefore, after a little thought, I suggested that the real difference in performance is in using lists for keys, not strings. Lines (and array) have much better cache consistency. So, I changed the key from the list "k" to a string and voila! Performance is now really better than the map in my application!
Here the code is
[<RequireQualifiedAccess>] module StringTrie type Node<'v> = { TrieMap : Map<char, Node<'v>> TrieKvp : (string * 'v) option } member inline x.IsEmpty = x.TrieKvp.IsNone && x.TrieMap.IsEmpty let inline make map kvp = { TrieMap = map TrieKvp = kvp } let inline makeEmpty () : Node<'v> = make Map.empty None let inline isEmpty (node : Node<'v>) = node.IsEmpty let rec tryFindInternal (key : string) index node = if key.Length = index then match node.TrieKvp with | Some (_, value) -> Some value | None -> None else let optSubNode = Map.tryFind key.[index] node.TrieMap match optSubNode with | Some subNode -> tryFindInternal key (index + 1) subNode | None -> None let inline tryFind (key : string) node = tryFindInternal key 0 node let inline containsKey key node = (tryFind key node).IsSome let rec addInternal (key : string) index value node = if key.Length = index then make node.TrieMap (Some (key, value)) else let char = key.[index] let newTrie = match Map.tryFind char node.TrieMap with | Some subTrie -> subTrie | None -> makeEmpty () let newTrie2 = addInternal key (index + 1) value newTrie make (Map.add char newTrie2 node.TrieMap) node.TrieKvp let inline add key value node = addInternal key 0 value node let rec addMany kvps node = if Seq.isEmpty kvps then node else let kvpHead = Seq.head kvps let kvpTail = Seq.skip 1 kvps let newTrie = add (fst kvpHead) (snd kvpHead) node addMany kvpTail newTrie let inline ofList kvps = addMany kvps (makeEmpty ()) let inline ofListBy by kvps = let pairs = List.map by kvps ofList pairs let rec foldInternal folder rev node state = match node.TrieKvp with | Some (_, value) -> folder (Map.fold (fun state key value -> foldInternal folder (key :: rev) value state) state node.TrieMap) (List.rev rev) value | None -> Map.fold (fun state key value -> foldInternal folder (key :: rev) value state) state node.TrieMap let inline fold folder state node = foldInternal folder [] node state let rec map (mapper : string -> 'v -> 'a) (node : Node<'v>) : Node<'a> = match node.TrieKvp with | Some (key, value) -> make (Map.map (fun _ value -> map mapper value) node.TrieMap) (Some (key, mapper key value)) | None -> make (Map.map (fun _ value -> map mapper value) node.TrieMap) None let inline toValueList node = fold (fun state _ value -> value :: state) [] node let inline singleton (key, value) = add key value (makeEmpty ())
I also built a version that works for arrays in general, and also fast -
[<RequireQualifiedAccess>] module ArrayTrie type Node<'k, 'v when 'k : comparison> = { TrieMap : Map<'k, Node<'k, 'v>> TrieKvp : ('k array * 'v) option } member inline x.IsEmpty = x.TrieKvp.IsNone && x.TrieMap.IsEmpty let inline make map kvp = { TrieMap = map TrieKvp = kvp } let inline makeEmpty () : Node<'k, 'v> = make Map.empty None let inline isEmpty (node : Node<'k, 'v>) = node.IsEmpty let rec tryFindInternal (key : 'k array) index node = if key.Length = index then match node.TrieKvp with | Some (_, value) -> Some value | None -> None else let optSubNode = Map.tryFind key.[index] node.TrieMap match optSubNode with | Some subNode -> tryFindInternal key (index + 1) subNode | None -> None let inline tryFind (key : 'k array) node = tryFindInternal key 0 node let inline containsKey key node = (tryFind key node).IsSome let rec addInternal (key : 'k array) index value node = if key.Length = index then make node.TrieMap (Some (key, value)) else let char = key.[index] let newTrie = match Map.tryFind char node.TrieMap with | Some subTrie -> subTrie | None -> makeEmpty () let newTrie2 = addInternal key (index + 1) value newTrie make (Map.add char newTrie2 node.TrieMap) node.TrieKvp let inline add key value node = addInternal key 0 value node let rec addMany kvps node = if Seq.isEmpty kvps then node else let kvpHead = Seq.head kvps let kvpTail = Seq.skip 1 kvps let newTrie = add (fst kvpHead) (snd kvpHead) node addMany kvpTail newTrie let inline ofList kvps = addMany kvps (makeEmpty ()) let inline ofListBy by kvps = let pairs = List.map by kvps ofList pairs let rec foldInternal folder rev node state = match node.TrieKvp with | Some (_, value) -> folder (Map.fold (fun state key value -> foldInternal folder (key :: rev) value state) state node.TrieMap) (List.rev rev) value | None -> Map.fold (fun state key value -> foldInternal folder (key :: rev) value state) state node.TrieMap let inline fold folder state node = foldInternal folder [] node state let rec map (mapper : 'k array -> 'v -> 'a) (node : Node<'k, 'v>) : Node<'k, 'a> = match node.TrieKvp with | Some (key, value) -> make (Map.map (fun _ value -> map mapper value) node.TrieMap) (Some (key, mapper key value)) | None -> make (Map.map (fun _ value -> map mapper value) node.TrieMap) None let inline toValueList node = fold (fun state _ value -> value :: state) [] node let inline singleton (key, value) = add key value (makeEmpty ())
The only thing that seems to improve performance is to get an internal pointer to a string and, rather, instead of doing indexes over and over again. This does not seem easy in F #, but at least it is possible for arrays in C #.