diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7b34ad4 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +.PHONY: bench +bench: + dune exec bench/replay.exe lru 5001 \ No newline at end of file diff --git a/bench/dune b/bench/dune index 13fd944..5689a84 100644 --- a/bench/dune +++ b/bench/dune @@ -1,7 +1,19 @@ (executable + (modules main) (name main) (libraries bechamel bechamel-notty notty.unix lru cachecache)) +(executable + (modules + lru_trace_definition + replay + trace_auto_file_format_intf + trace_auto_file_format) + (preprocess + (pps ppx_repr)) + (name replay) + (libraries cachecache fmt ppx_repr repr mtime mtime.clock.os cmdliner)) + (alias (name runtest) (package cachecache-bench) diff --git a/bench/lru_trace_definition.ml b/bench/lru_trace_definition.ml new file mode 100644 index 0000000..c82153b --- /dev/null +++ b/bench/lru_trace_definition.ml @@ -0,0 +1,29 @@ +(** File format of a trace listing the interactions with repr's LRU *) + +module V0 = struct + let version = 0 + + type header = { instance_count : int } [@@deriving repr] + + type op = Add of string | Find of string | Mem of string | Clear + [@@deriving repr] + + type row = { instance_id : int; op : op } [@@deriving repr] +end + +module Latest = V0 +include Latest + +include Trace_auto_file_format.Make (struct + module Latest = Latest + + let magic = Trace_auto_file_format.Magic.of_string "LRUtrace" + + let get_version_converter = function + | 0 -> + Trace_auto_file_format.create_version_converter ~header_t:V0.header_t + ~row_t:V0.row_t ~upgrade_header:Fun.id ~upgrade_row:Fun.id + | i -> + let msg = Fmt.str "Unknown Raw_actions_trace version %d" i in + failwith msg +end) diff --git a/bench/replay.ml b/bench/replay.ml new file mode 100644 index 0000000..d3e63d7 --- /dev/null +++ b/bench/replay.ml @@ -0,0 +1,117 @@ +type span = Mtime.span + +let span_t = Repr.map Repr.float (fun _ -> assert false) Mtime.Span.to_s + +type stats = { + mutable add : int; + mutable mem : int; + mutable find : int; + mutable hit : int; + mutable miss : int; + (* mutable discard : int; *) + mutable add_span : span; + mutable mem_span : span; + mutable find_span : span; + mutable total_runtime_span : span; +} +[@@deriving repr ~pp] + +module K = struct + include String + + let hash = Hashtbl.hash +end + +let pr_bench test_name metric_name value = + Format.printf + {|{"results": [{"name": "%s", "metrics": [{"name": "%s", "value": %f, "units": "ms"}]}]}@.|} + test_name metric_name value + +module Make (Cache : Cachecache.S.Cache with type key = K.t) = struct + let bench cap = + let stats = + { + add = 0; + mem = 0; + find = 0; + hit = 0; + miss = 0; + (* discard = 0; *) + add_span = Mtime.Span.zero; + mem_span = Mtime.Span.zero; + find_span = Mtime.Span.zero; + total_runtime_span = Mtime.Span.zero; + } + in + let open Lru_trace_definition in + let _, { instance_count }, seq = + open_reader "/home/cha//Downloads/lru.trace" + in + let caches = List.init instance_count (fun _ -> Cache.v cap) in + let counter = Mtime_clock.counter () in + Seq.iter + (fun { instance_id; op } -> + let cache = List.nth caches instance_id in + match op with + | Add k -> + let before = Mtime_clock.count counter in + Cache.replace cache k (); + let after = Mtime_clock.count counter in + stats.total_runtime_span <- + Mtime.Span.(abs_diff after before |> add stats.total_runtime_span); + stats.add_span <- + Mtime.Span.(abs_diff after before |> add stats.add_span); + stats.add <- stats.add + 1 + | Find k -> + let before = Mtime_clock.count counter in + ignore (Cache.find_opt cache k : _ option); + let after = Mtime_clock.count counter in + stats.total_runtime_span <- + Mtime.Span.(abs_diff after before |> add stats.total_runtime_span); + stats.find_span <- + Mtime.Span.(abs_diff after before |> add stats.find_span); + stats.find <- stats.find + 1 + | Mem k -> + let before = Mtime_clock.count counter in + let b = Cache.mem cache k in + let after = Mtime_clock.count counter in + stats.total_runtime_span <- + Mtime.Span.(abs_diff after before |> add stats.total_runtime_span); + stats.mem_span <- + Mtime.Span.(abs_diff after before |> add stats.mem_span); + if b then stats.hit <- stats.hit + 1 + else stats.miss <- stats.miss + 1; + stats.mem <- stats.mem + 1 + | _ -> assert false) + seq; + (* Fmt.pr "%a\n" pp_stats stats *) + pr_bench "add" "add_metric" (Mtime.Span.to_ms stats.add_span); + pr_bench "mem" "mem_metric" (Mtime.Span.to_ms stats.mem_span); + pr_bench "find" "find_metric" (Mtime.Span.to_ms stats.find_span); + pr_bench "total_runtime" "total_runtime_metric" + (Mtime.Span.to_ms stats.total_runtime_span) +end + +include Cachecache.Lru.Make (K) +module Lru = Cachecache.Lru.Make (K) +module Lfu = Cachecache.Lfu.Make (K) +module Bench_lru = Make (Lru) +module Bench_lfu = Make (Lfu) + +let main algo cap = + match algo with `Lru -> Bench_lru.bench cap | `Lfu -> Bench_lfu.bench cap + +open Cmdliner + +let algo = + let l = [ ("lru", `Lru); ("lfu", `Lfu) ] in + let i = Arg.info [] in + Arg.(required @@ pos 0 (some (enum l)) None i) + +let cap = + let i = Arg.info [] in + Arg.(required @@ pos 1 (some int) None i) + +let main_t = Term.(const main $ algo $ cap) +let cmd = Cmd.v (Cmd.info "replay") main_t +let () = exit (Cmd.eval cmd) \ No newline at end of file diff --git a/bench/trace_auto_file_format.ml b/bench/trace_auto_file_format.ml new file mode 100644 index 0000000..5db1cd8 --- /dev/null +++ b/bench/trace_auto_file_format.ml @@ -0,0 +1,213 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2021-2022 Tarides *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +include Trace_auto_file_format_intf + +type ('latest_header, 'latest_row, 'header, 'row) version_converter' = { + header_t : 'header Repr.ty; + row_t : 'row Repr.ty; + upgrade_header : 'header -> 'latest_header; + upgrade_row : 'row -> 'latest_row; +} +(** Contains everything needed to read a file as if it is written using the + lastest version. *) + +(** A box containing the above record *) +type (_, _) version_converter = + | Version_converter : + ('latest_header, 'latest_row, 'header, 'row) version_converter' + -> ('latest_header, 'latest_row) version_converter + +let create_version_converter : + header_t:'header Repr.ty -> + row_t:'row Repr.ty -> + upgrade_header:('header -> 'latest_header) -> + upgrade_row:('row -> 'latest_row) -> + ('latest_header, 'latest_row) version_converter = + fun ~header_t ~row_t ~upgrade_header ~upgrade_row -> + Version_converter { header_t; row_t; upgrade_header; upgrade_row } + +module Magic : MAGIC = struct + type t = string + + let of_string s = + if String.length s <> 8 then + invalid_arg "Magic.of_string, string should have 8 chars"; + s + + let to_string s = s + let pp ppf s = Format.fprintf ppf "%s" (String.escaped s) +end + +module type FILE_FORMAT = + FILE_FORMAT + with type magic := Magic.t + with type ('a, 'b) version_converter := ('a, 'b) version_converter + +module type S = + S + with type File_format.magic := Magic.t + with type ('a, 'b) File_format.version_converter := + ('a, 'b) version_converter + +(** Variable size integer. Very similar to what can be found in + "repr/type_binary.ml", but working straight off channels. [Var_int.read_exn] + reads the chars one by one from the provided [chan]. The recursion stops as + soon as a read char has its 8th bit equal to [0]. [Var_int.write] could be + implemented using [Repr.encode_bin int], but since [read_exn] can't be + implemented using repr, [write] isn't either. *) +module Var_int = struct + let chars = + Array.init 256 (fun i -> Bytes.unsafe_to_string (Bytes.make 1 (Char.chr i))) + + let write : int -> out_channel -> unit = + let int i k = + let rec aux n k = + if n >= 0 && n < 128 then k chars.(n) + else + let out = 128 lor (n land 127) in + k chars.(out); + aux (n lsr 7) k + in + aux i k + in + fun i chan -> int i (output_string chan) + + let read_exn : in_channel -> int = + fun chan -> + let max_bits = Sys.word_size - 1 in + let rec aux n p = + let () = + if p >= max_bits then raise (failwith "Failed to decode varint") + in + let i = input_char chan |> Char.code in + let n = n + ((i land 127) lsl p) in + if i >= 0 && i < 128 then n else aux n (p + 7) + in + aux 0 0 +end + +module Make (Ff : FILE_FORMAT) = struct + module File_format = Ff + + let decode_i32 = Repr.(decode_bin int32 |> unstage) + let encode_i32 = Repr.(encode_bin int32 |> unstage) + let encode_lheader = Repr.(encode_bin Ff.Latest.header_t |> unstage) + let encode_lrow = Repr.(encode_bin Ff.Latest.row_t |> unstage) + + let read_with_prefix_exn : (string -> int ref -> 'a) -> in_channel -> 'a = + fun decode chan -> + (* First read the prefix *) + let len = Var_int.read_exn chan in + (* Then read the repr. *) + let offset_ref = ref 0 in + let v = + (* This could fail if [len] is not long enough for repr (corruption) *) + decode (really_input_string chan len) offset_ref + in + let () = + if len <> !offset_ref then + let msg = + Fmt.str + "A value read in the Trace was expected to take %d bytes, but it \ + took only %d." + len !offset_ref + in + raise (failwith msg) + in + v + + let decoded_seq_of_encoded_chan_with_prefixes : + 'a Repr.ty -> in_channel -> 'a Seq.t = + fun repr chan -> + let decode = Repr.decode_bin repr |> Repr.unstage in + let produce_row () = + try + let row = read_with_prefix_exn decode chan in + Some (row, ()) + with End_of_file -> None + in + Seq.unfold produce_row () + + let open_reader : string -> int * Ff.Latest.header * Ff.Latest.row Seq.t = + fun path -> + let chan = open_in_bin path in + let len = LargeFile.in_channel_length chan in + let () = + if len < 12L then + let msg = Fmt.str "File '%s' should be at least 12 byte long" path in + raise (failwith msg) + in + + let magic = Magic.of_string (really_input_string chan 8) in + let () = + if magic <> Ff.magic then + let msg = + Fmt.str "File '%s' has magic '%a'. Expected '%a'." path Magic.pp magic + Magic.pp Ff.magic + in + raise (failwith msg) + in + + let offset_ref = ref 0 in + let version = decode_i32 (really_input_string chan 4) offset_ref in + let (Version_converter vc) = + assert (!offset_ref = 4); + Ff.get_version_converter (Int32.to_int version) + in + + let header = + let decode_header = Repr.(decode_bin vc.header_t |> unstage) in + read_with_prefix_exn decode_header chan |> vc.upgrade_header + in + let seq = + decoded_seq_of_encoded_chan_with_prefixes vc.row_t chan + |> Seq.map vc.upgrade_row + in + (Int32.to_int version, header, seq) + + type writer = { channel : out_channel; buffer : Buffer.t } + + let create channel header = + let buffer = Buffer.create 0 in + output_string channel (Magic.to_string Ff.magic); + encode_i32 (Int32.of_int Ff.Latest.version) (output_string channel); + encode_lheader header (Buffer.add_string buffer); + Var_int.write (Buffer.length buffer) channel; + output_string channel (Buffer.contents buffer); + Buffer.clear buffer; + { channel; buffer } + + let create_file path header = create (open_out path) header + + let append_row { channel; buffer; _ } row = + encode_lrow row (Buffer.add_string buffer); + Var_int.write (Buffer.length buffer) channel; + output_string channel (Buffer.contents buffer); + Buffer.clear buffer + + let flush { channel; _ } = flush channel + let close { channel; _ } = close_out channel +end diff --git a/bench/trace_auto_file_format_intf.ml b/bench/trace_auto_file_format_intf.ml new file mode 100644 index 0000000..78bc0af --- /dev/null +++ b/bench/trace_auto_file_format_intf.ml @@ -0,0 +1,113 @@ +(*****************************************************************************) +(* *) +(* Open Source License *) +(* Copyright (c) 2021-2022 Tarides *) +(* *) +(* Permission is hereby granted, free of charge, to any person obtaining a *) +(* copy of this software and associated documentation files (the "Software"),*) +(* to deal in the Software without restriction, including without limitation *) +(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *) +(* and/or sell copies of the Software, and to permit persons to whom the *) +(* Software is furnished to do so, subject to the following conditions: *) +(* *) +(* The above copyright notice and this permission notice shall be included *) +(* in all copies or substantial portions of the Software. *) +(* *) +(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*) +(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *) +(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *) +(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*) +(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *) +(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *) +(* DEALINGS IN THE SOFTWARE. *) +(* *) +(*****************************************************************************) + +(** Utility to simplify the management of files using the following layout: + + - Magic (Magic.t, 8 bytes) + - Version (int32, 4 bytes) + - Length of header (varint, >=1 byte) + - Header (header_t, _ bytes) + - Arbitrary long series of rows, of unspecified length, each prefixed by + their length: + - Length of row (varint, >=1 byte) + - Row (row_t, _ bytes) *) + +module type MAGIC = sig + type t + + val of_string : string -> t + val to_string : t -> string + val pp : Format.formatter -> t -> unit +end + +(** Manually defined *) +module type FILE_FORMAT = sig + (** The latest up-to-date definition of the file format *) + module Latest : sig + val version : int + + type header [@@deriving repr] + type row [@@deriving repr] + end + + type magic + + val magic : magic + + type ('a, 'b) version_converter + + val get_version_converter : + int -> (Latest.header, Latest.row) version_converter + (** [get_version_converter v] is a converter that can upgrade headers and rows + from a version [i] to [Latest.version]. It may raise [Invalid_argument] if *) +end + +(** Automatically defined *) +module type S = sig + module File_format : FILE_FORMAT + + val open_reader : + string -> int * File_format.Latest.header * File_format.Latest.row Seq.t + + type writer + + val create : out_channel -> File_format.Latest.header -> writer + val create_file : string -> File_format.Latest.header -> writer + val append_row : writer -> File_format.Latest.row -> unit + val flush : writer -> unit + val close : writer -> unit +end + +module type Trace_auto_file_format = sig + type ('a, 'b) version_converter + + val create_version_converter : + header_t:'header Repr.ty -> + row_t:'row Repr.ty -> + upgrade_header:('header -> 'latest_header) -> + upgrade_row:('row -> 'latest_row) -> + ('latest_header, 'latest_row) version_converter + (** Create a value that contains everything needed to upgrade on-the-fly a + file version to the latest version defined by the file format. *) + + module type MAGIC = MAGIC + + module Magic : MAGIC + + module type FILE_FORMAT = + FILE_FORMAT + with type magic := Magic.t + with type ('a, 'b) version_converter := ('a, 'b) version_converter + + module type S = + S + with type File_format.magic := Magic.t + with type ('a, 'b) File_format.version_converter := + ('a, 'b) version_converter + + (** Derive the IO operations from a file format. *) + module Make (File_format : FILE_FORMAT) : + S with module File_format = File_format +end diff --git a/cachecache-bench.opam b/cachecache-bench.opam index b1791a7..b47c563 100644 --- a/cachecache-bench.opam +++ b/cachecache-bench.opam @@ -10,10 +10,12 @@ bug-reports: "https://github.com/pascutto/cachecache/issues" depends: [ "dune" {>= "2.0"} "ocaml" {>= "4.07.1"} + "ppx_repr" + "mtime.clock.os" "bechamel" {with-test} "bechamel-notty" {with-test} "notty" {with-test} - "lru" {with-test} + "lru" ] build: [ ["dune" "subst"] {pinned} diff --git a/cachecache.opam b/cachecache.opam index ad5687f..3cd02d3 100644 --- a/cachecache.opam +++ b/cachecache.opam @@ -30,6 +30,8 @@ depends: [ "dune" {>= "2.0"} "ocaml" {>= "4.07.1"} "fmt" + "ppx_repr" + "mtime.clock.os" "alcotest" {with-test} "gospel" {with-test} "fpath" {with-test} diff --git a/src/cachecache.ml b/src/cachecache.ml index 22eb78a..bd74d20 100644 --- a/src/cachecache.ml +++ b/src/cachecache.ml @@ -1,3 +1,4 @@ module Lru = Lru module Lfu = Lfu +module S = S module Through = Through diff --git a/src/chunk_lst.ml b/src/chunk_lst.ml new file mode 100644 index 0000000..5cca134 --- /dev/null +++ b/src/chunk_lst.ml @@ -0,0 +1 @@ +type 'a t = { contents : 'a array; free : int } diff --git a/src/dbllist.ml b/src/dbllist.ml index a85b8e1..e5f86e2 100644 --- a/src/dbllist.ml +++ b/src/dbllist.ml @@ -53,7 +53,8 @@ let append_after l c v = new_cell let clear l = l := Nil -let get l = match !l with Nil -> assert false | List l -> (l.first, l.last) +let ends l = match !l with Nil -> assert false | List l -> (l.first, l.last) +let get c = c.content let remove t c = match !t with diff --git a/src/dbllist.mli b/src/dbllist.mli index dea08c5..3a94b1a 100644 --- a/src/dbllist.mli +++ b/src/dbllist.mli @@ -12,5 +12,6 @@ val append : 'a t -> 'a -> 'a cell val append_before : 'a t -> 'a cell -> 'a -> 'a cell val append_after : 'a t -> 'a cell -> 'a -> 'a cell val clear : 'a t -> unit -val get : 'a t -> 'a cell * 'a cell +val ends : 'a t -> 'a cell * 'a cell val remove : 'a t -> 'a cell -> unit +val get : 'a cell -> 'a diff --git a/src/dllist.ml b/src/dllist.ml index ff50f6a..5d74ca5 100644 --- a/src/dllist.ml +++ b/src/dllist.ml @@ -1,87 +1,139 @@ type 'a t = { - contents : 'a array; + cap : int; witness : 'a; + contents : 'a array; + mutable free : int; prev : int array; next : int array; +} + +type 'a l = { mutable first : int; mutable last : int; - mutable free : int; - cap : int; mutable size : int; + t : 'a t; } -let create c witness = +type 'a c = int + +let status ppf l = + Fmt.pf ppf "first : %d@\n" l.first; + Fmt.pf ppf "last : %d@\n" l.last; + Fmt.(pf ppf "free : %a@\n" int) l.t.free; + Fmt.(pf ppf "prev : %a@\n" (array ~sep:sp int)) l.t.prev; + Fmt.(pf ppf "next : %a@\n@\n" (array ~sep:sp int)) l.t.next + +let is_full l = l.t.free == -1 + +let create cap witness = { - contents = Array.make c witness; + cap; witness; - prev = Array.init c pred; - next = Array.init c (fun i -> if i = c - 1 then -1 else succ i); - first = -1; - last = -1; + contents = Array.make cap witness; free = 0; - cap = c; - size = 0; + prev = Array.init cap pred; + next = Array.init cap (fun i -> if i = cap - 1 then -1 else succ i); } -let clear t = - t.first <- -1; - t.last <- -1; - t.free <- 0; - t.size <- 0; - let o = t.cap - 1 in - let o1 = 0 in - for i = o1 to o do - t.contents.(i) <- t.witness; - t.prev.(i) <- -1; - t.next.(i) <- -1 - done +let create_list t = { first = -1; last = -1; size = 0; t } + +let next l i = + let n = l.t.next.(i) in + if n = -1 then i else n + +let ends l = (l.first, l.last) -let append t v = +let clear l = + let rec aux i = + if i = -1 then () + else ( + l.t.contents.(i) <- l.t.witness; + l.t.prev.(i) <- -1; + l.t.next.(i) <- -1; + aux l.t.next.(i)) + in + aux l.first; + l.first <- -1; + l.last <- -1; + l.size <- 0 + +let append l v = let removed = - if t.free <> -1 then ( - let index = t.free in - t.free <- t.next.(t.free); - if t.free <> -1 then t.prev.(t.free) <- -1; - t.next.(index) <- t.first; - if t.size = 0 then t.last <- index else t.prev.(t.first) <- index; - t.first <- index; - t.contents.(index) <- v; - t.size <- t.size + 1; + if l.t.free <> -1 then ( + let index = l.t.free in + l.t.free <- l.t.next.(l.t.free); + if l.t.free <> -1 then l.t.prev.(l.t.free) <- -1; + l.t.next.(index) <- -1; + if l.size = 0 then l.first <- index else l.t.next.(l.last) <- index; + l.t.prev.(index) <- l.last; + l.last <- index; + l.t.contents.(index) <- v; + l.size <- l.size + 1; None) else - let removed = Some t.contents.(t.last) in - let new_first = t.last in - t.last <- t.prev.(t.last); - t.contents.(new_first) <- v; - t.next.(t.last) <- -1; - t.prev.(new_first) <- -1; - t.next.(new_first) <- t.first; - t.prev.(t.first) <- new_first; - t.first <- new_first; + let removed = Some l.t.contents.(l.first) in + let index = l.first in + l.t.prev.(l.t.next.(l.first)) <- -1; + l.first <- l.t.next.(l.first); + l.t.prev.(index) <- l.last; + l.t.next.(l.last) <- index; + l.last <- index; + l.t.contents.(index) <- v; + l.t.next.(index) <- -1; removed in - (t.first, removed) + (l.last, removed) + +let promote l i = + if i <> l.last then ( + l.t.prev.(l.t.next.(i)) <- l.t.prev.(i); + if i = l.first then l.first <- l.t.next.(i) + else l.t.next.(l.t.prev.(i)) <- l.t.next.(i); + l.t.next.(l.last) <- i; + l.t.prev.(i) <- l.last; + l.t.next.(i) <- -1; + l.last <- i); + l.last + +let remove l i = + if i = l.first then l.first <- l.t.next.(l.first) + else l.t.next.(l.t.prev.(i)) <- l.t.next.(i); + if i = l.last then l.last <- l.t.prev.(l.last) + else l.t.prev.(l.t.next.(i)) <- l.t.prev.(i); + if l.t.free <> -1 then l.t.prev.(l.t.free) <- i; + l.t.next.(i) <- l.t.free; + l.t.prev.(i) <- -1; + l.t.free <- i; + l.size <- l.size - 1; + l.t.contents.(i) <- l.t.witness -let promote t i = - if i <> t.first then ( - t.next.(t.prev.(i)) <- t.next.(i); - if i <> t.last then t.prev.(t.next.(i)) <- t.prev.(i) - else t.last <- t.prev.(i); - t.prev.(t.first) <- i; - t.next.(i) <- t.first; - t.prev.(i) <- -1; - t.first <- i); - t.first +let get l i = l.t.contents.(i) +let length l = l.size +let is_empty l = l.size = 0 -let remove t i = - if i <> t.first then t.next.(t.prev.(i)) <- t.next.(i); - if i <> t.last then t.prev.(t.next.(i)) <- t.prev.(i) - else t.last <- t.prev.(t.last); - if t.free <> -1 then t.prev.(t.free) <- i; - t.next.(i) <- t.free; - t.prev.(i) <- -1; - t.free <- i; - t.size <- t.size - 1 +let append_before l i v = + let new_index = l.t.free in + l.t.free <- l.t.next.(l.t.free); + if l.t.free <> -1 then l.t.prev.(l.t.free) <- -1; + if l.first = i then l.first <- new_index + else l.t.next.(l.t.prev.(i)) <- new_index; + l.t.next.(new_index) <- i; + l.t.prev.(new_index) <- l.t.prev.(i); + l.t.prev.(i) <- new_index; + l.t.contents.(new_index) <- v; + l.size <- l.size + 1; + new_index -let get t i1 = t.contents.(i1) -let length t = t.size +let append_after l i v = + let new_index = l.t.free in + assert (new_index <> -1); + l.t.free <- l.t.next.(l.t.free); + if l.t.free <> -1 then l.t.prev.(l.t.free) <- -1; + if l.last = i then l.last <- new_index + else l.t.prev.(l.t.next.(i)) <- new_index; + l.t.prev.(new_index) <- i; + l.t.next.(new_index) <- l.t.next.(i); + l.t.next.(i) <- new_index; + l.t.contents.(new_index) <- v; + l.size <- l.size + 1; + new_index diff --git a/src/dllist.mli b/src/dllist.mli index 2b3f93a..a7a19be 100644 --- a/src/dllist.mli +++ b/src/dllist.mli @@ -1,9 +1,34 @@ -type 'a t +type 'a t = private { + cap : int; + witness : 'a; + contents : 'a array; + mutable free : int; + prev : int array; + next : int array; +} +(*@ invariant cap > 0 *) + +type 'a l = private { + mutable first : int; + mutable last : int; + mutable size : int; + t : 'a t; +} + +type 'a c = int val create : int -> 'a -> 'a t -val length : 'a t -> int -val append : 'a t -> 'a -> int * 'a option -val promote : 'a t -> int -> int -val remove : 'a t -> int -> unit -val get : 'a t -> int -> 'a -val clear : 'a t -> unit +val create_list : 'a t -> 'a l +val length : 'a l -> int +val append : 'a l -> 'a -> 'a c * 'a option +val promote : 'a l -> 'a c -> 'a c +val remove : 'a l -> 'a c -> unit +val get : 'a l -> 'a c -> 'a +val clear : 'a l -> unit +val is_empty : 'a l -> bool +val append_before : 'a l -> 'a c -> 'a -> 'a c +val append_after : 'a l -> 'a c -> 'a -> 'a c +val next : 'a l -> 'a c -> 'a c +val ends : 'a l -> 'a c * 'a c +val status : Format.formatter -> 'a l -> unit +val is_full : 'a l -> bool diff --git a/src/lfu.ml b/src/lfu.ml index 58033bb..23357fb 100644 --- a/src/lfu.ml +++ b/src/lfu.ml @@ -8,23 +8,23 @@ struct module H = Hashtbl.Make (K) type key = K.t - type freq_cell = (int * key Dbllist.t) Dbllist.cell - type key_cell = key Dbllist.cell + (* type freq_index = int + type key_index = int *) type 'a t = { - value : (freq_cell * key_cell * 'a) H.t; - frequency : (int * key Dbllist.t) Dbllist.t; + lsts : key Dllist.t; + value : ((int * key Dllist.l) Dllist.c * key Dllist.c * 'a) H.t; + frequency : (int * key Dllist.l) Dllist.l; cap : int; stats : Stats.t; } + let dummy = Obj.magic (ref 0) + let unsafe_v c = - { - value = H.create c; - frequency = Dbllist.create (); - cap = c; - stats = Stats.v (); - } + let lsts = Dllist.create c dummy in + let freq = Dllist.create c dummy |> Dllist.create_list in + { lsts; value = H.create c; frequency = freq; cap = c; stats = Stats.v () } let v c = if c <= 0 then invalid_arg "capacity must be strictly positive"; @@ -37,31 +37,48 @@ struct let clear t = H.clear t.value; - Dbllist.clear t.frequency; + Dllist.clear t.frequency; Stats.clear t.stats let update t k = - let freq_cell, key_cell, _value = H.find t.value k in - let freq, freq_list = freq_cell.content in - let freq_next, _freq_next_list = freq_cell.next.content in - (if freq <> freq_next - 1 then - let real_next_freq_list = Dbllist.create () in - let real_freq = freq + 1 in - ignore - (Dbllist.append_after t.frequency freq_cell - (real_freq, real_next_freq_list) - : freq_cell)); - Dbllist.remove freq_list key_cell; - let _freq_next, freq_next_list = freq_cell.next.content in - if Dbllist.is_empty freq_list then Dbllist.remove t.frequency freq_cell; - let last = Dbllist.append freq_next_list k in - (freq_cell.next, last) + let freq_index, key_index, _value = H.find t.value k in + let freq, freq_list = Dllist.get t.frequency freq_index in + let next = Dllist.next t.frequency freq_index in + let freq_next, _freq_next_list = Dllist.get t.frequency next in + + Dllist.remove freq_list key_index; + let new_freq_index = + if freq_next = freq + 1 then ( + if Dllist.is_empty freq_list then Dllist.remove t.frequency freq_index; + next) + else + let new_next = Dllist.create_list t.lsts in + if not (Dllist.is_full t.frequency) then ( + let r = + Dllist.append_after t.frequency freq_index (freq + 1, new_next) + in + if Dllist.is_empty freq_list then Dllist.remove t.frequency freq_index; + r) + else ( + Dllist.remove t.frequency freq_index; + let res = + if next = -1 then + let r, _opt = Dllist.append t.frequency (freq + 1, new_next) in + r + else Dllist.append_before t.frequency next (freq + 1, new_next) + in + res) + in + let _freq_next, freq_next_list = Dllist.get t.frequency new_freq_index in + let new_key_index, _opt = Dllist.append freq_next_list k in + assert (_opt = None); + (new_freq_index, new_key_index) let find t k = - let _freq_cell, _key_cell, v = H.find t.value k in + let _freq_index, _key_index, v = H.find t.value k in Stats.hit t.stats; - let new_freq_cell, new_last_cell = update t k in - H.replace t.value k (new_freq_cell, new_last_cell, v); + let new_freq_index, new_key_index = update t k in + H.replace t.value k (new_freq_index, new_key_index, v); v let find_opt t k = @@ -79,53 +96,60 @@ struct false let add t k v = - if H.length t.value = 0 then - let first_freq_list = Dbllist.create () in - let new_cell = Dbllist.append first_freq_list k in - let first_freq_cell = Dbllist.append t.frequency (1, first_freq_list) in - H.replace t.value k (first_freq_cell, new_cell, v) + if H.length t.value = 0 then ( + let first_freq_list = Dllist.create_list t.lsts in + let new_key_index, _opt = Dllist.append first_freq_list k in + assert (_opt = None); + let first_key_index, _opt = + Dllist.append t.frequency (1, first_freq_list) + in + assert (_opt = None); + H.replace t.value k (first_key_index, new_key_index, v)) else - let first_freq_cell, _last_freq_cell = Dbllist.get t.frequency in - let freq, _freq_list = first_freq_cell.content in - (if freq <> 1 then - let real_first_freq_list = Dbllist.create () in - ignore - (Dbllist.append_before t.frequency first_freq_cell + let first_freq_index, _last_freq_index = Dllist.ends t.frequency in + let freq, _key_list = Dllist.get t.frequency first_freq_index in + let new_first_freq_index = + if freq = 1 then first_freq_index + else + let real_first_freq_list = Dllist.create_list t.lsts in + Dllist.append_before t.frequency first_freq_index (1, real_first_freq_list) - : freq_cell)); - let first_freq_cell, _last_freq_cell = Dbllist.get t.frequency in - let _freq, freq_list = first_freq_cell.content in - let new_cell = Dbllist.append freq_list k in - H.replace t.value k (first_freq_cell, new_cell, v) + in + let _freq, freq_list = Dllist.get t.frequency new_first_freq_index in + let new_index, _opt = Dllist.append freq_list k in + assert (_opt = None); + H.replace t.value k (new_first_freq_index, new_index, v) let replace t k v = try - let _freq_cell, _key_cell, _value = H.find t.value k in - let new_freq_cell, new_last_cell = update t k in + let _freq_index, _key_index, _value = H.find t.value k in + let new_freq_index, new_key_index = update t k in Stats.replace t.stats; - H.replace t.value k (new_freq_cell, new_last_cell, v) + H.replace t.value k (new_freq_index, new_key_index, v) with Not_found -> Stats.add (H.length t.value + 1) t.stats; if H.length t.value < t.cap then add t k v else - let first_freq_cell, _last_freq_cell = Dbllist.get t.frequency in - let _freq, freq_list = first_freq_cell.content in - let first_cell, _last_cell = Dbllist.get freq_list in - Dbllist.remove freq_list first_cell; - if Dbllist.is_empty freq_list then - Dbllist.remove t.frequency first_freq_cell; - let remove_key = first_cell.content in + let first_freq_index, _last_freq_index = Dllist.ends t.frequency in + let _freq, freq_list = Dllist.get t.frequency first_freq_index in + assert (not (Dllist.is_empty freq_list)); + let first_index, _last_index = Dllist.ends freq_list in + + let remove_key = Dllist.get freq_list first_index in + Dllist.remove freq_list first_index; + if Dllist.is_empty freq_list then + Dllist.remove t.frequency first_freq_index; H.remove t.value remove_key; Stats.discard t.stats; add t k v let remove t k = try - let freq_cell, key_cell, _value = H.find t.value k in + let freq_index, key_index, _value = H.find t.value k in H.remove t.value k; - let _freq, freq_list = freq_cell.content in - Dbllist.remove freq_list key_cell; - if Dbllist.is_empty freq_list then Dbllist.remove t.frequency freq_cell; + let _freq, key_list = Dllist.get t.frequency freq_index in + Dllist.remove key_list key_index; + if Dllist.is_empty key_list then Dllist.remove t.frequency freq_index; Stats.remove t.stats with Not_found -> () end diff --git a/src/lru.ml b/src/lru.ml index 34f23ad..edeb0d0 100644 --- a/src/lru.ml +++ b/src/lru.ml @@ -12,8 +12,8 @@ struct let dummy : K.t = Obj.magic (ref 0) type 'a t = { - tbl : (int * 'a) H.t; - lst : K.t Dllist.t; + tbl : (K.t Dllist.c * 'a) H.t; + lst : K.t Dllist.l; cap : int; stats : Stats.t; } @@ -21,7 +21,7 @@ struct let unsafe_v c = { tbl = H.create c; - lst = Dllist.create c dummy; + lst = Dllist.create c dummy |> Dllist.create_list; cap = c; stats = Stats.v (); } diff --git a/test/lfu.ml b/test/lfu.ml index 4327cbb..0c4c278 100644 --- a/test/lfu.ml +++ b/test/lfu.ml @@ -88,8 +88,8 @@ struct | h :: tl -> List.iter (fun k -> - Alcotest.(check bool) - "[Direct] Added values are still present" true (Cache.mem t k)) + Alcotest.(check key) + "[Direct] Added values are still present" k (Cache.find t k)) tl; let k = K.v () in Cache.replace t k k;