From 1f3736e690f57fb853fb8418229d6527679b6cda Mon Sep 17 00:00:00 2001 From: Humza Shahid Date: Tue, 28 May 2024 08:01:10 +0100 Subject: [PATCH] add benchmarks for tiny_rope in bench folder --- .gitignore | 10 +- bench/Makefile | 21 +- bench/benchmarks.md | 4 + bench/gap_buffer_automerge.sml | 2 - bench/gap_buffer_rust.sml | 2 - bench/gap_buffer_seph.sml | 2 - bench/gap_buffer_svelte.sml | 2 - bench/rope_automerge.mlb | 13 + bench/rope_automerge.sml | 13 + bench/rope_rust.mlb | 13 + bench/rope_rust.sml | 13 + bench/rope_seph.mlb | 13 + bench/rope_seph.sml | 13 + bench/rope_svelte.mlb | 13 + bench/rope_svelte.sml | 13 + bench/run.sml | 33 +-- bench/transaction.sml | 2 - src/unrolled_gap_buffer.sml | 452 +++++++++++++++++++++++++++++++++ 18 files changed, 585 insertions(+), 49 deletions(-) create mode 100644 bench/benchmarks.md create mode 100644 bench/rope_automerge.mlb create mode 100644 bench/rope_automerge.sml create mode 100644 bench/rope_rust.mlb create mode 100644 bench/rope_rust.sml create mode 100644 bench/rope_seph.mlb create mode 100644 bench/rope_seph.sml create mode 100644 bench/rope_svelte.mlb create mode 100644 bench/rope_svelte.sml create mode 100644 src/unrolled_gap_buffer.sml diff --git a/.gitignore b/.gitignore index 56d2701..c4f4a45 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,9 @@ -/bench/out +/bench/gap_buffer_svelte +/bench/gap_buffer_rust +/bench/gap_buffer_seph +/bench/gap_buffer_automerge + +/bench/rope_svelte +/bench/rope_rust +/bench/rope_seph +/bench/rope_automerge diff --git a/bench/Makefile b/bench/Makefile index 9efb384..b0a1787 100644 --- a/bench/Makefile +++ b/bench/Makefile @@ -1,8 +1,5 @@ -bench: gap_buffer_svelte gap_buffer_rust gap_buffer_seph gap_buffer_automerge - ./gap_buffer_svelte - ./gap_buffer_rust - ./gap_buffer_seph - ./gap_buffer_automerge +bench: gap_buffer_svelte gap_buffer_rust gap_buffer_seph gap_buffer_automerge rope_svelte rope_rust rope_seph rope_automerge + hyperfine './gap_buffer_svelte' './rope_svelte' './gap_buffer_rust' './rope_rust' './gap_buffer_seph' './rope_seph' './gap_buffer_automerge' './rope_automerge' --export-markdown benchmarks.md gap_buffer_svelte: mlton gap_buffer_svelte.mlb @@ -16,5 +13,17 @@ gap_buffer_seph: gap_buffer_automerge: mlton gap_buffer_automerge.mlb +rope_svelte: + mlton rope_svelte.mlb + +rope_rust: + mlton rope_rust.mlb + +rope_seph: + mlton rope_seph.mlb + +rope_automerge: + mlton rope_automerge.mlb + clean: - rm -f gap_buffer_svelte gap_buffer_rust gap_buffer_seph gap_buffer_automerge + rm -f gap_buffer_svelte gap_buffer_rust gap_buffer_seph gap_buffer_automerge rope_svelte rope_rust rope_seph rope_automerge diff --git a/bench/benchmarks.md b/bench/benchmarks.md new file mode 100644 index 0000000..f4316d2 --- /dev/null +++ b/bench/benchmarks.md @@ -0,0 +1,4 @@ +| Command | Mean [ms] | Min [ms] | Max [ms] | Relative | +|:---|---:|---:|---:|---:| +| `./gap_buffer_svelte` | 15.9 ± 7.4 | 4.9 | 34.2 | 1.00 | +| `./rope_svelte` | 20.5 ± 9.9 | 8.5 | 39.3 | 1.29 ± 0.87 | diff --git a/bench/gap_buffer_automerge.sml b/bench/gap_buffer_automerge.sml index 5e4e318..3ebd325 100644 --- a/bench/gap_buffer_automerge.sml +++ b/bench/gap_buffer_automerge.sml @@ -5,8 +5,6 @@ struct val insert = GapBuffer.insert val delete = GapBuffer.delete val toString = GapBuffer.toString - - val title = "Automerge" val txns = AutomergePaper.txns end diff --git a/bench/gap_buffer_rust.sml b/bench/gap_buffer_rust.sml index 001b965..f164a71 100644 --- a/bench/gap_buffer_rust.sml +++ b/bench/gap_buffer_rust.sml @@ -5,8 +5,6 @@ struct val insert = GapBuffer.insert val delete = GapBuffer.delete val toString = GapBuffer.toString - - val title = "Rust" val txns = RustCode.txns end diff --git a/bench/gap_buffer_seph.sml b/bench/gap_buffer_seph.sml index 0b181a0..56f696d 100644 --- a/bench/gap_buffer_seph.sml +++ b/bench/gap_buffer_seph.sml @@ -5,8 +5,6 @@ struct val insert = GapBuffer.insert val delete = GapBuffer.delete val toString = GapBuffer.toString - - val title = "Seph" val txns = SephBlog.txns end diff --git a/bench/gap_buffer_svelte.sml b/bench/gap_buffer_svelte.sml index 15cf1bf..a97d0a8 100644 --- a/bench/gap_buffer_svelte.sml +++ b/bench/gap_buffer_svelte.sml @@ -5,8 +5,6 @@ struct val insert = GapBuffer.insert val delete = GapBuffer.delete val toString = GapBuffer.toString - - val title = "Svelte" val txns = SvelteComponent.txns end diff --git a/bench/rope_automerge.mlb b/bench/rope_automerge.mlb new file mode 100644 index 0000000..13c7c1e --- /dev/null +++ b/bench/rope_automerge.mlb @@ -0,0 +1,13 @@ +$(SML_LIB)/basis/basis.mlb + +ann + "allowVectorExps true" +in + automerge.sml +end + +transaction.sml +run.sml + +../src/tiny_rope.sml +rope_automerge.sml diff --git a/bench/rope_automerge.sml b/bench/rope_automerge.sml new file mode 100644 index 0000000..7f75c8c --- /dev/null +++ b/bench/rope_automerge.sml @@ -0,0 +1,13 @@ +structure RopeAutomerge: TRANSACTION = +struct + type t = TinyRope.t + val empty = TinyRope.empty + val insert = TinyRope.insert + val delete = TinyRope.delete + val toString = TinyRope.toString + val txns = AutomergePaper.txns +end + +structure Main = Run(RopeAutomerge) + +val _ = Main.run () diff --git a/bench/rope_rust.mlb b/bench/rope_rust.mlb new file mode 100644 index 0000000..6024f56 --- /dev/null +++ b/bench/rope_rust.mlb @@ -0,0 +1,13 @@ +$(SML_LIB)/basis/basis.mlb + +ann + "allowVectorExps true" +in + rust.sml +end + +transaction.sml +run.sml + +../src/tiny_rope.sml +rope_rust.sml diff --git a/bench/rope_rust.sml b/bench/rope_rust.sml new file mode 100644 index 0000000..28bae65 --- /dev/null +++ b/bench/rope_rust.sml @@ -0,0 +1,13 @@ +structure RopeRust: TRANSACTION = +struct + type t = TinyRope.t + val empty = TinyRope.empty + val insert = TinyRope.insert + val delete = TinyRope.delete + val toString = TinyRope.toString + val txns = RustCode.txns +end + +structure Main = Run(RopeRust) + +val _ = Main.run () diff --git a/bench/rope_seph.mlb b/bench/rope_seph.mlb new file mode 100644 index 0000000..f47101c --- /dev/null +++ b/bench/rope_seph.mlb @@ -0,0 +1,13 @@ +$(SML_LIB)/basis/basis.mlb + +ann + "allowVectorExps true" +in + seph.sml +end + +transaction.sml +run.sml + +../src/tiny_rope.sml +rope_seph.sml diff --git a/bench/rope_seph.sml b/bench/rope_seph.sml new file mode 100644 index 0000000..c3fcd24 --- /dev/null +++ b/bench/rope_seph.sml @@ -0,0 +1,13 @@ +structure RopeSeph: TRANSACTION = +struct + type t = TinyRope.t + val empty = TinyRope.empty + val insert = TinyRope.insert + val delete = TinyRope.delete + val toString = TinyRope.toString + val txns = SephBlog.txns +end + +structure Main = Run(RopeSeph) + +val _ = Main.run () diff --git a/bench/rope_svelte.mlb b/bench/rope_svelte.mlb new file mode 100644 index 0000000..eb35b3f --- /dev/null +++ b/bench/rope_svelte.mlb @@ -0,0 +1,13 @@ +$(SML_LIB)/basis/basis.mlb + +ann + "allowVectorExps true" +in + svelte.sml +end + +transaction.sml +run.sml + +../src/tiny_rope.sml +rope_svelte.sml diff --git a/bench/rope_svelte.sml b/bench/rope_svelte.sml new file mode 100644 index 0000000..fa8b83b --- /dev/null +++ b/bench/rope_svelte.sml @@ -0,0 +1,13 @@ +structure RopeSvelte: TRANSACTION = +struct + type t = TinyRope.t + val empty = TinyRope.empty + val insert = TinyRope.insert + val delete = TinyRope.delete + val toString = TinyRope.toString + val txns = SvelteComponent.txns +end + +structure Main = Run(RopeSvelte) + +val _ = Main.run () diff --git a/bench/run.sml b/bench/run.sml index 265ddf2..7edafd3 100644 --- a/bench/run.sml +++ b/bench/run.sml @@ -14,40 +14,9 @@ struct Vector.foldl folder Txn.empty Txn.txns end - fun runTxnsTime () = - let - val startTime = Time.now () - val startTime = Time.toMilliseconds startTime - - val x = runTxns () - - val endTime = Time.now () - val endTime = Time.toMilliseconds endTime - - val timeDiff = endTime - startTime - val timeDiff = LargeInt.toString timeDiff - val timeTook = String.concat [timeDiff, " ms taken for " ^ Txn.title ^ " txns\n"] - val _ = (print timeTook) - in - x - end - - fun write (fileName, buffer) = - let - val str = Txn.toString buffer - val io = TextIO.openOut fileName - val _ = TextIO.output (io, str) - val _ = TextIO.closeOut io - in - () - end - fun run () = let - val buffer = runTxnsTime () - (* The write operation guarantees that MLton doesn't optimise away the - * buffer's contents, because it has to write the contents to a file. *) - val _ = write ("out/" ^ Txn.title ^ ".txt", buffer) + val buffer = runTxns () in () end diff --git a/bench/transaction.sml b/bench/transaction.sml index d71cdd3..633c421 100644 --- a/bench/transaction.sml +++ b/bench/transaction.sml @@ -5,7 +5,5 @@ sig val insert: int * string * t -> t val delete: int * int * t -> t val toString: t -> string - - val title : string val txns : (int * int * string) vector end diff --git a/src/unrolled_gap_buffer.sml b/src/unrolled_gap_buffer.sml new file mode 100644 index 0000000..b669c78 --- /dev/null +++ b/src/unrolled_gap_buffer.sml @@ -0,0 +1,452 @@ +signature GAP_BUFFER = +sig + type t + val empty: t + val fromString: string -> t + val toString: t -> string + val insert: int * string * t -> t + val delete: int * int * t -> t +end + +structure GapBuffer: GAP_BUFFER = +struct + type t = {idx: int, left: string vector list, right: string vector list} + + val stringLimit = 1025 + val vectorLimit = 33 + + val empty = {idx = 0, left = Vector.fromList [], right = Vector.fromList []} + + fun fromString string = + { idx = String.size string + , left = Vector.fromList [string] + , right = Vector.fromList [] + } + + local + fun fromVectorToList (pos, acc, vec) = + if pos >= 0 then + let val acc = (Vector.sub (vec, pos) :: acc) + in fromVectorToList (pos - 1, acc, vec) + end + else + acc + + fun toList (acc, input) = + case input of + hd :: tl => + let val acc = fromVectorToList (Vector.length hd - 1, acc, hd) + in toList (acc, tl) + end + | [] => acc + in + fun toString ({left, right, ...}: t) = + let + val lst = toList ([], List.rev right) + val lst = toList (lst, left) + in + String.concat lst + end + end + + fun isStringInLimit2 (s1, s2) = + String.size s1 + String.size s2 < stringLimit + + fun isStringInLimit3 (s1, s2, s3) = + String.size s1 + String.size s2 + String.size s3 < stringLimit + + fun isVecInLimit2 (v1, v2) = + Vector.length v1 + Vector.length v2 < vectorLimit + + fun joinEndOfLeft (newVec, left) = + case left of + hd :: tail => + if isVecInLimit2 (newVec, hd) then + let + val startLength = Vector.length hd + val endLength = startLength + Vector.length newVec + val newVector = Vector.tabulate (endLength, fn idx => + if idx < startLength then Vector.sub (left, idx) + else Vector.sub (newVec, idx - startLength)) + in + newVector :: tail + end + else + newVec :: left + | [] => newVec :: left + + fun joinStartOfRight (newVec, right) = + case right of + hd :: tail => + if isStringInLimit2 (hd, newVec) then + let + val startLength = Vector.length newVec + val endLength = startLength + Vector.length hd + val newVector = Vector.tabulate (endLength, fn idx => + if idx < startLength then Vector.sub (newVec, idx) + else Vector.sub (hd, idx - startLength)) + in + newVector :: tail + end + else + newVec :: right + | [] => newVec :: right + + fun preferInsertLeft (curIdx, newVec, left, right) = + case left of + hd :: tail => + if isVecInLimit2 (hd, newVec) then + { idx = curIdx + String.size newString + , left = (hd ^ newString) :: tail + , right = right + } + else + (case right of + hd :: tail => + if isStringInLimit2 (hd, newString) then + {idx = curIdx, left = left, right = (newString ^ hd) :: tail} + else + joinEndOfLeft (curIdx, newString, left, right) + | [] => joinEndOfLeft (curIdx, newString, left, right)) + | [] => joinEndOfLeft (curIdx, newString, left, right) + + fun insLeft (prevIdx, idx, newString, curIdx, hd, tail, right) = + (* The requested index is either: + * - At the start of the left string + * - In the middle of the left string + * Find out which and split the middle of the string if necessary. *) + if idx = prevIdx then + (* At start of string. *) + { idx = curIdx + String.size newString + , right = right + , left = + (* These two meant to look reversed, + * with respect to newString and hd. + * + * The line + * `newString ^ hd` + * places the contents of newString before hd, + * and the line + * `hd :: newString` + * in a zipper also places newString before hd. + * + * Using `newString ^ hd` with `newString :: hd` gives + * different contents in the case of a zipper. + * *) + if isStringInLimit2 (newString, hd) then (newString ^ hd) :: tail + else hd :: newString :: tail + } + else + (* In middle of string. *) + let + val length = idx - prevIdx + val sub1 = String.substring (hd, 0, length) + val sub2 = String.substring (hd, length, String.size hd - length) + in + if isStringInLimit3 (sub1, newString, sub2) then + { idx = curIdx + String.size newString + , left = (sub1 ^ newString ^ sub2) :: tail + , right = right + } + else if isStringInLimit2 (sub1, newString) then + { idx = prevIdx + String.size sub1 + String.size newString + , left = (sub1 ^ newString) :: tail + , right = joinStartOfRight (sub2, right) + } + else if isStringInLimit2 (newString, sub2) then + { idx = prevIdx + String.size sub1 + , left = joinEndOfLeft (sub1, tail) + , right = (newString ^ sub2) :: right + } + else + { idx = prevIdx + , left = tail + , right = sub1 :: newString :: sub2 :: right + } + end + + fun insRight (nextIdx, idx, newString, curIdx, left, hd, tail) = + if idx = nextIdx then + (* At end of next string. *) + { idx = curIdx + , left = left + , right = + if isStringInLimit2 (newString, hd) then (hd ^ newString) :: tail + else hd :: (joinStartOfRight (newString, tail)) + } + else + let + val length = idx - curIdx + val sub1 = String.substring (hd, 0, length) + val sub2 = String.substring (hd, length, String.size hd - length) + in + if isStringInLimit3 (sub1, newString, sub2) then + { idx = + curIdx + String.size sub1 + String.size newString + + String.size sub2 + , left = (sub1 ^ newString ^ sub2) :: left + , right = tail + } + else if isStringInLimit2 (sub1, newString) then + { idx = curIdx + String.size sub1 + String.size newString + , left = (sub1 ^ newString) :: left + , right = joinStartOfRight (sub2, tail) + } + else if isStringInLimit2 (newString, sub2) then + { idx = curIdx + String.size sub1 + , left = sub1 :: left + , right = (newString ^ sub2) :: tail + } + else + { idx = curIdx + String.size sub1 + String.size newString + , left = newString :: sub1 :: left + , right = joinStartOfRight (sub2, tail) + } + end + + + fun ins (idx, newString, curIdx, left, right) : t = + if curIdx = idx then + preferInsertLeft (curIdx, newString, left, right) + else if idx < curIdx then + (* Need to insert on the left. *) + case left of + [] => + (* If there is no string on the left, then add the new string there. *) + {idx = String.size newString, left = [newString], right = right} + | hd :: tail => + let + val prevIdx = curIdx - String.size hd + in + if idx < prevIdx then + (* The requested index is prior to the string on the left, + * so move leftward one string. *) + ins (idx, newString, prevIdx, tail, joinStartOfRight (hd, right)) + else + insLeft (prevIdx, idx, newString, curIdx, hd, tail, right) + end + else + (* Need to insert to the right. *) + case right of + [] => {idx = curIdx, left = left, right = [newString]} + | hd :: tail => + let + val nextIdx = String.size hd + curIdx + in + if idx > nextIdx then + ins (idx, newString, nextIdx, joinEndOfLeft (hd, left), tail) + else + insRight (nextIdx, idx, newString, curIdx, left, hd, tail) + end + + fun insert (idx, newString, buffer: t) = + ins (idx, newString, #idx buffer, #left buffer, #right buffer) + + fun deleteRightFromHere (curIdx, finish, right) = + case right of + hd :: tail => + let + val nextIdx = curIdx + String.size hd + in + if nextIdx < finish then + deleteRightFromHere (nextIdx, finish, tail) + else if nextIdx > finish then + let + val newStrStart = finish - curIdx + val newStr = String.substring + (hd, newStrStart, String.size hd - newStrStart) + in + newStr :: tail + end + else + (* nextIdx = finish + * Delete current head but no further. *) + tail + end + | [] => right + + fun moveRightAndDelete (start, finish, curIdx, left, right) = + case right of + hd :: tail => + let + val nextIdx = curIdx + String.size hd + in + if nextIdx < start then + (* Keep moving right: haven't reached start yet. *) + moveRightAndDelete + (start, finish, nextIdx, joinEndOfLeft (hd, left), tail) + else if nextIdx > start then + if nextIdx < finish then + (* Delete the start range contained in this string, + * and then continue deleting right. *) + let + val length = start - curIdx + val newString = String.substring (hd, 0, length) + in + { idx = curIdx + String.size newString + , left = joinEndOfLeft (newString, left) + , right = deleteRightFromHere (nextIdx, finish, tail) + } + end + else if nextIdx > finish then + (* Have to delete from middle of string. *) + let + val sub1Length = start - curIdx + val sub1 = String.substring (hd, 0, sub1Length) + val sub2Start = finish - curIdx + val sub2 = String.substring + (hd, sub2Start, String.size hd - sub2Start) + in + { idx = curIdx + sub1Length + , left = joinEndOfLeft (sub1, left) + , right = joinStartOfRight (sub2, tail) + } + end + else + (* nextIdx = finish + * Have to delete from end of this string. *) + let + val strLength = start - curIdx + val str = String.substring (hd, 0, strLength) + in + { idx = curIdx + strLength + , left = joinEndOfLeft (str, left) + , right = tail + } + end + else + (* nextIdx = start + * The start range is contained fully at the next node, + * without having to remove part of a string at this node.*) + let + val newRight = deleteRightFromHere (nextIdx, finish, tail) + in + { idx = curIdx + , left = left + , right = joinStartOfRight (hd, newRight) + } + end + end + | [] => {idx = curIdx, left = left, right = right} + + fun deleteLeftFromHere (start, curIdx, left, right) = + case left of + hd :: tail => + let + val prevIdx = curIdx - String.size hd + in + if start < prevIdx then + deleteLeftFromHere (start, prevIdx, tail, right) + else if start > prevIdx then + (* Need to delete from some part of this string. *) + let + val length = start - prevIdx + val newStr = String.substring (hd, 0, length) + in + { idx = prevIdx + , left = tail + , right = joinStartOfRight (newStr, right) + } + end + else + (* if start = prevIdx + * Need to remove the current node without deleting any further. *) + {idx = prevIdx, left = tail, right = right} + end + + | [] => {idx = curIdx, left = left, right = right} + + fun deleteFromLeftAndRight (start, finish, curIdx, left, right) = + let val right = deleteRightFromHere (curIdx, finish, right) + in deleteLeftFromHere (start, curIdx, left, right) + end + + fun moveLeftAndDelete (start, finish, curIdx, left, right) = + case left of + hd :: tail => + let + val prevIdx = curIdx - String.size hd + in + if prevIdx > finish then + moveLeftAndDelete + (start, finish, prevIdx, tail, joinStartOfRight (hd, right)) + else if prevIdx < finish then + if prevIdx > start then + (* Delete from start point of this string, + * and then call function to continue deleting leftward. *) + let + val hdStart = finish - prevIdx + val newHd = String.substring + (hd, hdStart, String.size hd - hdStart) + val right = joinStartOfRight (newHd, right) + in + deleteLeftFromHere (start, prevIdx, tail, right) + end + else if prevIdx < start then + (* We want to delete in the middle of this current string. *) + let + val sub1Length = start - prevIdx + val sub1 = String.substring (hd, 0, sub1Length) + val sub2Start = finish - prevIdx + val sub2 = String.substring + (hd, sub2Start, String.size hd - sub2Start) + in + { idx = prevIdx + sub1Length + , left = joinEndOfLeft (sub1, tail) + , right = joinStartOfRight (sub2, right) + } + end + else + (* prevIdx = start + * We want to delete from the start of this string and stop. *) + let + val strStart = finish - prevIdx + val str = String.substring + (hd, strStart, String.size hd - strStart) + in + { idx = prevIdx + , left = tail + , right = joinStartOfRight (str, right) + } + end + else + (* prevIdx = finish *) + deleteLeftFromHere + (start, prevIdx, tail, joinStartOfRight (hd, right)) + end + | [] => {idx = curIdx, left = left, right = right} + + fun del (start, finish, curIdx, left, right) : t = + if start > curIdx then + (* If start is greater than current index, + * then finish must be greater too. + * Move buffer rightwards until finish is reached, + * and delete along the way. *) + moveRightAndDelete (start, finish, curIdx, left, right) + else if start < curIdx then + (* If start is less than current index, + * then finish could be either less than or equal/greater + * than the current index. + * We can treat equal/greater than as one case. *) + if finish <= curIdx then + (* Move leftward and delete along the way. *) + moveLeftAndDelete (start, finish, curIdx, left, right) + else + (* Delete rightward up to finish index, + * and then delete leftward until start index.*) + deleteFromLeftAndRight (start, finish, curIdx, left, right) + else + (* If start is equal to the current index, + * then only examine the right list. + * Just need to delete until reaching the finish index. *) + { idx = curIdx + , left = left + , right = deleteRightFromHere (curIdx, finish, right) + } + + fun delete (start, length, buffer: t) = + if length > 0 then + del (start, start + length, #idx buffer, #left buffer, #right buffer) + else + buffer +end