From 62a985b9371dcd79718699d2b64ff35a25d58448 Mon Sep 17 00:00:00 2001 From: Ariel Date: Mon, 18 Dec 2023 20:29:17 +0100 Subject: [PATCH 1/7] Instead of three colons, syntax is highlighted with three backticks --- doc/signatures.md | 55 +++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/doc/signatures.md b/doc/signatures.md index 8279622..2e7c60d 100644 --- a/doc/signatures.md +++ b/doc/signatures.md @@ -39,17 +39,20 @@ directly. There are a few ways you can approximate it. One way is to pass the Module name to the calling functions along with the data that it is going to be called on. - :::erlang - add(ModuleToUse, Key, Value, DictData) -> - ModuleToUse:add(Key, Value, DictData). + ```erlang + add(ModuleToUse, Key, Value, DictData) -> + ModuleToUse:add(Key, Value, DictData). + ``` This works, and you can vary how you want to pass the data. For example, you could easily use a tuple to contain the data. That is, you could pass in `{ModuleToUse, DictData}` and that would make it a bit cleaner. - :::erlang + + ```erlang add(Key, Value, {ModuleToUse, DictData}) -> - ModuleToUse:add(Key, Value, DictData). + ModuleToUse:add(Key, Value, DictData). + ``` Either way, there are a few problems with this approach. One of the biggest is that you lose code locality, by looking at this bit of code @@ -75,9 +78,10 @@ name. So what we actually want to do is something mole like this: - :::erlang + ```erlang add(Key, Value, DictData) -> dictionary:add(Key, Value, DictData). + ``` Doing this we retain the locality. We can easily look up the `dictionary` Module. We immediately have a good idea what a @@ -97,7 +101,7 @@ a [Behaviour](http://metajack.im/2008/10/29/custom-behaviors-in-erlang/) for our functionality. To continue our example we will define a Behaviour for dictionaries. That Behaviour looks like this: - :::erlang + ```erlang -module(ec_dictionary). -export([behaviour_info/1]). @@ -115,6 +119,7 @@ Behaviour for dictionaries. That Behaviour looks like this: {keys, 1}]; behaviour_info(_) -> undefined. + ``` So we have our Behaviour now. Unfortunately, this doesn't give us much @@ -124,7 +129,7 @@ dictionaries in an abstract way in our code. To do that we need to add a bit of functionality. We do that by actually implementing our own behaviour, starting with `new/1`. - :::erlang + ```erlang %% @doc create a new dictionary object from the specified module. The %% module should implement the dictionary behaviour. %% @@ -132,6 +137,7 @@ behaviour, starting with `new/1`. -spec new(module()) -> dictionary(_K, _V). new(ModuleName) when is_atom(ModuleName) -> #dict_t{callback = ModuleName, data = ModuleName:new()}. + ``` This code creates a new dictionary for us. Or to be more specific it actually creates a new dictionary Signature record, that will be used @@ -148,7 +154,7 @@ dictionary and another that just retrieves data. The first we will look at is the one that updates the dictionary by adding a value. - :::erlang + ```erlang %% @doc add a new value to the existing dictionary. Return a new %% dictionary containing the value. %% @@ -158,6 +164,7 @@ adding a value. -spec add(key(K), value(V), dictionary(K, V)) -> dictionary(K, V). add(Key, Value, #dict_t{callback = Mod, data = Data} = Dict) -> Dict#dict_t{data = Mod:add(Key, Value, Data)}. + ``` There are two key things here. @@ -173,7 +180,7 @@ implementation to do the work itself. Now lets do a data retrieval function. In this case, the `get` function of the dictionary Signature. - :::erlang + ```erlang %% @doc given a key return that key from the dictionary. If the key is %% not found throw a 'not_found' exception. %% @@ -183,6 +190,7 @@ of the dictionary Signature. -spec get(key(K), dictionary(K, V)) -> value(V). get(Key, #dict_t{callback = Mod, data = Data}) -> Mod:get(Key, Data). + ``` In this case, you can see a very similar approach to deconstructing the dict record. We still need to pull out the callback module and the @@ -236,7 +244,7 @@ We will take a look at one of the functions we have already seen. The semantics as any of the functions in the dict module. So a bit of translation needs to be done. We do that in the ec_dict module `get` function. - :::erlang + ```erlang -spec get(ec_dictionary:key(K), Object::dictionary(K, V)) -> ec_dictionary:value(V). get(Key, Data) -> @@ -246,6 +254,7 @@ translation needs to be done. We do that in the ec_dict module `get` function. error -> throw(not_found) end. + ``` So the ec_dict module's purpose for existence is to help the preexisting dict module implement the Behaviour defined by the @@ -267,12 +276,13 @@ create a couple of functions that create dictionaries for each type we want to test. The first we want to time is the Signature Wrapper, so `dict` vs `ec_dict` called as a Signature. - :::erlang + ```erlang create_dict() -> lists:foldl(fun(El, Dict) -> dict:store(El, El, Dict) end, dict:new(), lists:seq(1,100)). + ``` The only thing we do here is create a sequence of numbers 1 to 100, and then add each of those to the dict as an entry. We aren't too @@ -283,13 +293,14 @@ of the dictionaries themselves. We need to create a similar function for our Signature based dictionary `ec_dict`. - :::erlang + ```erlang create_dictionary(Type) -> lists:foldl(fun(El, Dict) -> ec_dictionary:add(El, El, Dict) end, ec_dictionary:new(Type), lists:seq(1,100)). + ``` Here we actually create everything using the Signature. So we don't need one function for each type. We can have one function that can @@ -302,7 +313,7 @@ data and one that returns data, just to get good coverage. For our dictionaries we are going to use the `size` function as well as the `add` function. - :::erlang + ```erlang time_direct_vs_signature_dict() -> io:format("Timing dict~n"), Dict = create_dict(), @@ -312,6 +323,7 @@ the `add` function. 1000000), io:format("Timing ec_dict implementation of ec_dictionary~n"), time_dict_type(ec_dict). + ``` The `test_avg` function runs the provided function the number of times specified in the second argument and collects timing information. We @@ -323,7 +335,7 @@ we don't have to hard code the calls for the Signature implementations. Lets take a look at the `time_dict_type` function. - :::erlang + ```erlang time_dict_type(Type) -> io:format("Testing ~p~n", [Type]), Dict = create_dictionary(Type), @@ -331,6 +343,7 @@ implementations. Lets take a look at the `time_dict_type` function. ec_dictionary:size(ec_dictionary:add(some_key, some_value, Dict)) end, 1000000). + ``` As you can see we take the type as an argument (we need it for `dict` creation) and call our create function. Then we run the same timings @@ -343,7 +356,7 @@ work for anything that implements that Signature. So we have our tests, what was the result. Well on my laptop this is what it looked like. - :::sh + ```sh Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false] Eshell V5.8.2 (abort with ^G) @@ -359,6 +372,7 @@ what it looked like. Median: 3 mics Average: 4 mics 2> + ``` So for the direct dict call, we average about 3 mics per call, while for the Signature Wrapper we average around 4. That's a 25% cost for @@ -373,12 +387,13 @@ Signature, but it is not a Signature Wrapper. It is a native implementation of the Signature. To use `ec_rbdict` directly we have to create a creation helper just like we did for dict. - :::erlang + ```erlang create_rbdict() -> lists:foldl(fun(El, Dict) -> ec_rbdict:add(El, El, Dict) end, ec_rbdict:new(), lists:seq(1,100)). + ``` This is exactly the same as `create_dict` with the exception that dict is replaced by `ec_rbdict`. @@ -387,7 +402,7 @@ The timing function itself looks very similar as well. Again notice that we have to hard code the concrete name for the concrete implementation, but we don't for the ec_dictionary test. - :::erlang + ```erlang time_direct_vs_signature_rbdict() -> io:format("Timing rbdict~n"), Dict = create_rbdict(), @@ -397,6 +412,7 @@ implementation, but we don't for the ec_dictionary test. 1000000), io:format("Timing ec_dict implementation of ec_dictionary~n"), time_dict_type(ec_rbdict). + ``` And there we have our test. What do the results look like? @@ -406,7 +422,7 @@ The main thing we are timing here is the additional cost of the dictionary Signature itself. Keep that in mind as we look at the results. - :::sh + ```sh Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false] Eshell V5.8.2 (abort with ^G) @@ -422,6 +438,7 @@ results. Median: 7 mics Average: 7 mics 2> + ``` So no difference it time. Well the reality is that there is a difference in timing, there must be, but we don't have enough From 6d4c471ff619602decf1c566b59d3da4e37cdb92 Mon Sep 17 00:00:00 2001 From: Ariel Date: Mon, 18 Dec 2023 20:31:49 +0100 Subject: [PATCH 2/7] Fixed typos and misnamings --- doc/signatures.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/doc/signatures.md b/doc/signatures.md index 2e7c60d..24315f7 100644 --- a/doc/signatures.md +++ b/doc/signatures.md @@ -2,10 +2,10 @@ Signatures ========== It often occurs in coding that we need a library, a set of -functionally. Often there are several algorithms that could provide -this functionality. However, the code that uses it, either doesn't +functionalities. Often there are several algorithms that could provide +each of these functionalities. However, the code that uses it, either doesn't care about the individual algorithm or wishes to delegate choosing -that algorithm to some higher level. Lets take the concrete example of +that algorithm to some higher level. Let's take the concrete example of dictionaries. A dictionary provides the ability to access a value via a key (other things as well but primarily this). There are may ways to implement a dictionary. Just a few are: @@ -234,15 +234,15 @@ purpose is to help a preexisting module implement the Behaviour defined by a Signature. A good example of this in our current example is the [erlware_commons/ec_dict](https://github.com/ericbmerritt/erlware_commons/blob/types/src/ec_dict.erl) -module. It implements the ec_dictionary Behaviour, but all the +module. It implements the `ec_dictionary` Behaviour, but all the functionality is provided by the [stdlib/dict](http://www.erlang.org/doc/man/dict.html) module itself. Let's take a look at one example to see how this is done. We will take a look at one of the functions we have already seen. The -`get` function in ec_dictionary doesn't have quite the same -semantics as any of the functions in the dict module. So a bit of -translation needs to be done. We do that in the ec_dict module `get` function. +`get` function in `ec_dictionary` doesn't have quite the same +semantics as any of the functions in the `dict` module. So a bit of +translation needs to be done. We do that in the `ec_dict:get/2` function. ```erlang -spec get(ec_dictionary:key(K), Object::dictionary(K, V)) -> @@ -256,8 +256,8 @@ translation needs to be done. We do that in the ec_dict module `get` function. end. ``` -So the ec_dict module's purpose for existence is to help the -preexisting dict module implement the Behaviour defined by the +So the `ec_dict` module's purpose for existence is to help the +preexisting `dict` module implement the Behaviour defined by the Signature. @@ -285,7 +285,7 @@ want to test. The first we want to time is the Signature Wrapper, so ``` The only thing we do here is create a sequence of numbers 1 to 100, -and then add each of those to the dict as an entry. We aren't too +and then add each of those to the `dict` as an entry. We aren't too worried about replicating real data in the dictionary. We care about timing the function call overhead of Signatures, not the performance of the dictionaries themselves. @@ -305,8 +305,8 @@ dictionary `ec_dict`. Here we actually create everything using the Signature. So we don't need one function for each type. We can have one function that can create anything that implements the Signature. That is the magic of -Signatures. Otherwise, this does the exact same thing as the dict -`create_dict/1`. +Signatures. Otherwise, this does the exact same thing as the dictionary +given by `create_dict/0`. We are going to use two function calls in our timing. One that updates data and one that returns data, just to get good coverage. For our @@ -347,7 +347,7 @@ implementations. Lets take a look at the `time_dict_type` function. As you can see we take the type as an argument (we need it for `dict` creation) and call our create function. Then we run the same timings -that we did for ec dict. In this case though, the type of dictionary +that we did for `ec_dict`. In this case though, the type of dictionary is never specified, we only ever call ec_dictionary, so this test will work for anything that implements that Signature. @@ -374,7 +374,7 @@ what it looked like. 2> ``` -So for the direct dict call, we average about 3 mics per call, while +So for the direct `dict` call, we average about 3 mics per call, while for the Signature Wrapper we average around 4. That's a 25% cost for Signature Wrappers in this example, for a very small number of calls. Depending on what you are doing that is going to be greater or @@ -400,7 +400,7 @@ is replaced by `ec_rbdict`. The timing function itself looks very similar as well. Again notice that we have to hard code the concrete name for the concrete -implementation, but we don't for the ec_dictionary test. +implementation, but we don't for the `ec_dictionary` test. ```erlang time_direct_vs_signature_rbdict() -> From 5c5c2642418bc208311c1bbc96660b034aa0dfc7 Mon Sep 17 00:00:00 2001 From: Ariel Date: Mon, 18 Dec 2023 20:33:17 +0100 Subject: [PATCH 3/7] For formulae, used Markdown math mode --- doc/signatures.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/signatures.md b/doc/signatures.md index 24315f7..3142c37 100644 --- a/doc/signatures.md +++ b/doc/signatures.md @@ -17,13 +17,13 @@ implement a dictionary. Just a few are: * Many, many more .... Each of these approaches has their own performance characteristics, -memory footprints etc. For example, a table of size n with open -addressing has no collisions and holds up to n elements, with a single -comparison for successful lookup, and a table of size n with chaining -and k keys has the minimum max(0, k-n) collisions and O(1 + k/n) +memory footprints, etc. For example, a table of size $n$ with open +addressing has no collisions and holds up to $n$ elements, with a single +comparison for successful lookup, and a table of size $n$_ with chaining +and $k$ keys has the minimum $\max(0, k-n)$ collisions and $\mathcal{O}(1 + k/n)$ comparisons for lookup. While for skip lists the performance characteristics are about as good as that of randomly-built binary -search trees - namely (O log n). So the choice of which to select +search trees - namely ($\mathcal{O}(\log n)$). So the choice of which to select depends very much on memory available, insert/read characteristics, etc. So delegating the choice to a single point in your code is a very good idea. Unfortunately, in Erlang that's so easy to do at the moment. From cd88825861250533858d6aff56a87a5ab9cecda1 Mon Sep 17 00:00:00 2001 From: Ariel Date: Mon, 18 Dec 2023 20:59:57 +0100 Subject: [PATCH 4/7] Syntax highlighted in code snippets --- doc/signatures.md | 290 +++++++++++++++++++++++----------------------- 1 file changed, 145 insertions(+), 145 deletions(-) diff --git a/doc/signatures.md b/doc/signatures.md index 3142c37..3e32c11 100644 --- a/doc/signatures.md +++ b/doc/signatures.md @@ -39,20 +39,20 @@ directly. There are a few ways you can approximate it. One way is to pass the Module name to the calling functions along with the data that it is going to be called on. - ```erlang - add(ModuleToUse, Key, Value, DictData) -> - ModuleToUse:add(Key, Value, DictData). - ``` +```erlang +add(ModuleToUse, Key, Value, DictData) -> + ModuleToUse:add(Key, Value, DictData). +``` This works, and you can vary how you want to pass the data. For example, you could easily use a tuple to contain the data. That is, you could pass in `{ModuleToUse, DictData}` and that would make it a bit cleaner. - ```erlang - add(Key, Value, {ModuleToUse, DictData}) -> - ModuleToUse:add(Key, Value, DictData). - ``` +```erlang +add(Key, Value, {ModuleToUse, DictData}) -> + ModuleToUse:add(Key, Value, DictData). +``` Either way, there are a few problems with this approach. One of the biggest is that you lose code locality, by looking at this bit of code @@ -78,10 +78,10 @@ name. So what we actually want to do is something mole like this: - ```erlang - add(Key, Value, DictData) -> - dictionary:add(Key, Value, DictData). - ``` +```erlang +add(Key, Value, DictData) -> + dictionary:add(Key, Value, DictData). +``` Doing this we retain the locality. We can easily look up the `dictionary` Module. We immediately have a good idea what a @@ -101,25 +101,25 @@ a [Behaviour](http://metajack.im/2008/10/29/custom-behaviors-in-erlang/) for our functionality. To continue our example we will define a Behaviour for dictionaries. That Behaviour looks like this: - ```erlang - -module(ec_dictionary). +```erlang +-module(ec_dictionary). - -export([behaviour_info/1]). +-export([behaviour_info/1]). - behaviour_info(callbacks) -> - [{new, 0}, - {has_key, 2}, - {get, 2}, - {add, 3}, - {remove, 2}, - {has_value, 2}, - {size, 1}, - {to_list, 1}, - {from_list, 1}, - {keys, 1}]; - behaviour_info(_) -> - undefined. - ``` +behaviour_info(callbacks) -> + [{new, 0}, + {has_key, 2}, + {get, 2}, + {add, 3}, + {remove, 2}, + {has_value, 2}, + {size, 1}, + {to_list, 1}, + {from_list, 1}, + {keys, 1}]; +behaviour_info(_) -> + undefined. +``` So we have our Behaviour now. Unfortunately, this doesn't give us much @@ -129,15 +129,15 @@ dictionaries in an abstract way in our code. To do that we need to add a bit of functionality. We do that by actually implementing our own behaviour, starting with `new/1`. - ```erlang - %% @doc create a new dictionary object from the specified module. The - %% module should implement the dictionary behaviour. - %% - %% @param ModuleName The module name. - -spec new(module()) -> dictionary(_K, _V). - new(ModuleName) when is_atom(ModuleName) -> - #dict_t{callback = ModuleName, data = ModuleName:new()}. - ``` +```erlang +%% @doc create a new dictionary object from the specified module. The +%% module should implement the dictionary behaviour. +%% +%% @param ModuleName The module name. +-spec new(module()) -> dictionary(_K, _V). +new(ModuleName) when is_atom(ModuleName) -> + #dict_t{callback = ModuleName, data = ModuleName:new()}. +``` This code creates a new dictionary for us. Or to be more specific it actually creates a new dictionary Signature record, that will be used @@ -154,17 +154,17 @@ dictionary and another that just retrieves data. The first we will look at is the one that updates the dictionary by adding a value. - ```erlang - %% @doc add a new value to the existing dictionary. Return a new - %% dictionary containing the value. - %% - %% @param Dict the dictionary object to add too - %% @param Key the key to add - %% @param Value the value to add - -spec add(key(K), value(V), dictionary(K, V)) -> dictionary(K, V). - add(Key, Value, #dict_t{callback = Mod, data = Data} = Dict) -> - Dict#dict_t{data = Mod:add(Key, Value, Data)}. - ``` +```erlang +%% @doc add a new value to the existing dictionary. Return a new +%% dictionary containing the value. +%% +%% @param Dict the dictionary object to add too +%% @param Key the key to add +%% @param Value the value to add +-spec add(key(K), value(V), dictionary(K, V)) -> dictionary(K, V). +add(Key, Value, #dict_t{callback = Mod, data = Data} = Dict) -> + Dict#dict_t{data = Mod:add(Key, Value, Data)}. +``` There are two key things here. @@ -180,17 +180,17 @@ implementation to do the work itself. Now lets do a data retrieval function. In this case, the `get` function of the dictionary Signature. - ```erlang - %% @doc given a key return that key from the dictionary. If the key is - %% not found throw a 'not_found' exception. - %% - %% @param Dict The dictionary object to return the value from - %% @param Key The key requested - %% @throws not_found when the key does not exist - -spec get(key(K), dictionary(K, V)) -> value(V). - get(Key, #dict_t{callback = Mod, data = Data}) -> - Mod:get(Key, Data). - ``` +```erlang +%% @doc given a key return that key from the dictionary. If the key is +%% not found throw a 'not_found' exception. +%% +%% @param Dict The dictionary object to return the value from +%% @param Key The key requested +%% @throws not_found when the key does not exist +-spec get(key(K), dictionary(K, V)) -> value(V). +get(Key, #dict_t{callback = Mod, data = Data}) -> + Mod:get(Key, Data). +``` In this case, you can see a very similar approach to deconstructing the dict record. We still need to pull out the callback module and the @@ -244,17 +244,17 @@ We will take a look at one of the functions we have already seen. The semantics as any of the functions in the `dict` module. So a bit of translation needs to be done. We do that in the `ec_dict:get/2` function. - ```erlang - -spec get(ec_dictionary:key(K), Object::dictionary(K, V)) -> - ec_dictionary:value(V). - get(Key, Data) -> - case dict:find(Key, Data) of - {ok, Value} -> - Value; +```erlang +-spec get(ec_dictionary:key(K), Object::dictionary(K, V)) -> + ec_dictionary:value(V). +get(Key, Data) -> + case dict:find(Key, Data) of + {ok, Value} -> + Value; error -> - throw(not_found) - end. - ``` + throw(not_found) + end. +``` So the `ec_dict` module's purpose for existence is to help the preexisting `dict` module implement the Behaviour defined by the @@ -276,13 +276,13 @@ create a couple of functions that create dictionaries for each type we want to test. The first we want to time is the Signature Wrapper, so `dict` vs `ec_dict` called as a Signature. - ```erlang - create_dict() -> - lists:foldl(fun(El, Dict) -> - dict:store(El, El, Dict) +```erlang +create_dict() -> + lists:foldl(fun(El, Dict) -> + dict:store(El, El, Dict) end, dict:new(), lists:seq(1,100)). - ``` +``` The only thing we do here is create a sequence of numbers 1 to 100, and then add each of those to the `dict` as an entry. We aren't too @@ -293,14 +293,14 @@ of the dictionaries themselves. We need to create a similar function for our Signature based dictionary `ec_dict`. - ```erlang - create_dictionary(Type) -> - lists:foldl(fun(El, Dict) -> - ec_dictionary:add(El, El, Dict) +```erlang +create_dictionary(Type) -> + lists:foldl(fun(El, Dict) -> + ec_dictionary:add(El, El, Dict) end, ec_dictionary:new(Type), lists:seq(1,100)). - ``` +``` Here we actually create everything using the Signature. So we don't need one function for each type. We can have one function that can @@ -313,17 +313,17 @@ data and one that returns data, just to get good coverage. For our dictionaries we are going to use the `size` function as well as the `add` function. - ```erlang - time_direct_vs_signature_dict() -> - io:format("Timing dict~n"), - Dict = create_dict(), - test_avg(fun() -> +```erlang +time_direct_vs_signature_dict() -> + io:format("Timing dict~n"), + Dict = create_dict(), + test_avg(fun() -> dict:size(dict:store(some_key, some_value, Dict)) end, 1000000), - io:format("Timing ec_dict implementation of ec_dictionary~n"), - time_dict_type(ec_dict). - ``` + io:format("Timing ec_dict implementation of ec_dictionary~n"), + time_dict_type(ec_dict). +``` The `test_avg` function runs the provided function the number of times specified in the second argument and collects timing information. We @@ -335,15 +335,15 @@ we don't have to hard code the calls for the Signature implementations. Lets take a look at the `time_dict_type` function. - ```erlang - time_dict_type(Type) -> - io:format("Testing ~p~n", [Type]), - Dict = create_dictionary(Type), - test_avg(fun() -> - ec_dictionary:size(ec_dictionary:add(some_key, some_value, Dict)) - end, - 1000000). - ``` +```erlang +time_dict_type(Type) -> + io:format("Testing ~p~n", [Type]), + Dict = create_dictionary(Type), + test_avg(fun() -> + ec_dictionary:size(ec_dictionary:add(some_key, some_value, Dict)) + end, + 1000000). +``` As you can see we take the type as an argument (we need it for `dict` creation) and call our create function. Then we run the same timings @@ -356,23 +356,23 @@ work for anything that implements that Signature. So we have our tests, what was the result. Well on my laptop this is what it looked like. - ```sh - Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false] +```sh +Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false] - Eshell V5.8.2 (abort with ^G) +Eshell V5.8.2 (abort with ^G) - 1> ec_timing:time_direct_vs_signature_dict(). - Timing dict - Range: 2 - 5621 mics - Median: 3 mics - Average: 3 mics - Timing ec_dict implementation of ec_dictionary - Testing ec_dict - Range: 3 - 6097 mics - Median: 3 mics - Average: 4 mics - 2> - ``` +1> ec_timing:time_direct_vs_signature_dict(). +Timing dict +Range: 2 - 5621 mics +Median: 3 mics +Average: 3 mics +Timing ec_dict implementation of ec_dictionary +Testing ec_dict +Range: 3 - 6097 mics +Median: 3 mics +Average: 4 mics +2> +``` So for the direct `dict` call, we average about 3 mics per call, while for the Signature Wrapper we average around 4. That's a 25% cost for @@ -387,13 +387,13 @@ Signature, but it is not a Signature Wrapper. It is a native implementation of the Signature. To use `ec_rbdict` directly we have to create a creation helper just like we did for dict. - ```erlang - create_rbdict() -> - lists:foldl(fun(El, Dict) -> - ec_rbdict:add(El, El, Dict) +```erlang +create_rbdict() -> + lists:foldl(fun(El, Dict) -> + ec_rbdict:add(El, El, Dict) end, ec_rbdict:new(), lists:seq(1,100)). - ``` +``` This is exactly the same as `create_dict` with the exception that dict is replaced by `ec_rbdict`. @@ -402,17 +402,17 @@ The timing function itself looks very similar as well. Again notice that we have to hard code the concrete name for the concrete implementation, but we don't for the `ec_dictionary` test. - ```erlang - time_direct_vs_signature_rbdict() -> - io:format("Timing rbdict~n"), - Dict = create_rbdict(), - test_avg(fun() -> - ec_rbdict:size(ec_rbdict:add(some_key, some_value, Dict)) - end, - 1000000), - io:format("Timing ec_dict implementation of ec_dictionary~n"), - time_dict_type(ec_rbdict). - ``` +```erlang +time_direct_vs_signature_rbdict() -> + io:format("Timing rbdict~n"), + Dict = create_rbdict(), + test_avg(fun() -> + ec_rbdict:size(ec_rbdict:add(some_key, some_value, Dict)) + end, + 1000000), + io:format("Timing ec_dict implementation of ec_dictionary~n"), + time_dict_type(ec_rbdict). +``` And there we have our test. What do the results look like? @@ -422,23 +422,23 @@ The main thing we are timing here is the additional cost of the dictionary Signature itself. Keep that in mind as we look at the results. - ```sh - Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false] - - Eshell V5.8.2 (abort with ^G) - - 1> ec_timing:time_direct_vs_signature_rbdict(). - Timing rbdict - Range: 6 - 15070 mics - Median: 7 mics - Average: 7 mics - Timing ec_dict implementation of ec_dictionary - Testing ec_rbdict - Range: 6 - 6013 mics - Median: 7 mics - Average: 7 mics - 2> - ``` +```sh +Erlang R14B01 (erts-5.8.2) [source] [64-bit] [smp:4:4] [rq:4] [async-threads:0] [hipe] [kernel-poll:false] + +Eshell V5.8.2 (abort with ^G) + +1> ec_timing:time_direct_vs_signature_rbdict(). +Timing rbdict +Range: 6 - 15070 mics +Median: 7 mics +Average: 7 mics +Timing ec_dict implementation of ec_dictionary +Testing ec_rbdict +Range: 6 - 6013 mics +Median: 7 mics +Average: 7 mics +2> +``` So no difference it time. Well the reality is that there is a difference in timing, there must be, but we don't have enough From 68e9bbcd0fd782eb906932de9654a9a29d8a786b Mon Sep 17 00:00:00 2001 From: Ariel Date: Mon, 18 Dec 2023 21:00:25 +0100 Subject: [PATCH 5/7] Typo in math mode --- doc/signatures.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/signatures.md b/doc/signatures.md index 3e32c11..07db33a 100644 --- a/doc/signatures.md +++ b/doc/signatures.md @@ -19,7 +19,7 @@ implement a dictionary. Just a few are: Each of these approaches has their own performance characteristics, memory footprints, etc. For example, a table of size $n$ with open addressing has no collisions and holds up to $n$ elements, with a single -comparison for successful lookup, and a table of size $n$_ with chaining +comparison for successful lookup, and a table of size $n$ with chaining and $k$ keys has the minimum $\max(0, k-n)$ collisions and $\mathcal{O}(1 + k/n)$ comparisons for lookup. While for skip lists the performance characteristics are about as good as that of randomly-built binary From 7e69d4949e1615b9fd15c27d2597765dbeb39615 Mon Sep 17 00:00:00 2001 From: Ariel Date: Mon, 18 Dec 2023 21:19:24 +0100 Subject: [PATCH 6/7] Replaced tab by four spaces --- doc/signatures.md | 118 +++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/doc/signatures.md b/doc/signatures.md index 07db33a..4ad716f 100644 --- a/doc/signatures.md +++ b/doc/signatures.md @@ -41,7 +41,7 @@ it is going to be called on. ```erlang add(ModuleToUse, Key, Value, DictData) -> - ModuleToUse:add(Key, Value, DictData). + ModuleToUse:add(Key, Value, DictData). ``` This works, and you can vary how you want to pass the data. For @@ -51,7 +51,7 @@ bit cleaner. ```erlang add(Key, Value, {ModuleToUse, DictData}) -> - ModuleToUse:add(Key, Value, DictData). + ModuleToUse:add(Key, Value, DictData). ``` Either way, there are a few problems with this approach. One of the @@ -80,7 +80,7 @@ So what we actually want to do is something mole like this: ```erlang add(Key, Value, DictData) -> - dictionary:add(Key, Value, DictData). + dictionary:add(Key, Value, DictData). ``` Doing this we retain the locality. We can easily look up the @@ -107,18 +107,18 @@ Behaviour for dictionaries. That Behaviour looks like this: -export([behaviour_info/1]). behaviour_info(callbacks) -> - [{new, 0}, - {has_key, 2}, - {get, 2}, - {add, 3}, - {remove, 2}, - {has_value, 2}, - {size, 1}, - {to_list, 1}, - {from_list, 1}, - {keys, 1}]; + [{new, 0}, + {has_key, 2}, + {get, 2}, + {add, 3}, + {remove, 2}, + {has_value, 2}, + {size, 1}, + {to_list, 1}, + {from_list, 1}, + {keys, 1}]; behaviour_info(_) -> - undefined. + undefined. ``` @@ -136,7 +136,7 @@ behaviour, starting with `new/1`. %% @param ModuleName The module name. -spec new(module()) -> dictionary(_K, _V). new(ModuleName) when is_atom(ModuleName) -> - #dict_t{callback = ModuleName, data = ModuleName:new()}. + #dict_t{callback = ModuleName, data = ModuleName:new()}. ``` This code creates a new dictionary for us. Or to be more specific it @@ -163,7 +163,7 @@ adding a value. %% @param Value the value to add -spec add(key(K), value(V), dictionary(K, V)) -> dictionary(K, V). add(Key, Value, #dict_t{callback = Mod, data = Data} = Dict) -> - Dict#dict_t{data = Mod:add(Key, Value, Data)}. + Dict#dict_t{data = Mod:add(Key, Value, Data)}. ``` There are two key things here. @@ -189,7 +189,7 @@ of the dictionary Signature. %% @throws not_found when the key does not exist -spec get(key(K), dictionary(K, V)) -> value(V). get(Key, #dict_t{callback = Mod, data = Data}) -> - Mod:get(Key, Data). + Mod:get(Key, Data). ``` In this case, you can see a very similar approach to deconstructing @@ -246,14 +246,14 @@ translation needs to be done. We do that in the `ec_dict:get/2` function. ```erlang -spec get(ec_dictionary:key(K), Object::dictionary(K, V)) -> - ec_dictionary:value(V). + ec_dictionary:value(V). get(Key, Data) -> - case dict:find(Key, Data) of - {ok, Value} -> - Value; - error -> - throw(not_found) - end. + case dict:find(Key, Data) of + {ok, Value} -> + Value; + error -> + throw(not_found) + end. ``` So the `ec_dict` module's purpose for existence is to help the @@ -278,10 +278,10 @@ want to test. The first we want to time is the Signature Wrapper, so ```erlang create_dict() -> - lists:foldl(fun(El, Dict) -> - dict:store(El, El, Dict) - end, dict:new(), - lists:seq(1,100)). + lists:foldl(fun(El, Dict) -> + dict:store(El, El, Dict) + end, dict:new(), + lists:seq(1,100)). ``` The only thing we do here is create a sequence of numbers 1 to 100, @@ -295,11 +295,11 @@ dictionary `ec_dict`. ```erlang create_dictionary(Type) -> - lists:foldl(fun(El, Dict) -> - ec_dictionary:add(El, El, Dict) - end, - ec_dictionary:new(Type), - lists:seq(1,100)). + lists:foldl(fun(El, Dict) -> + ec_dictionary:add(El, El, Dict) + end, + ec_dictionary:new(Type), + lists:seq(1,100)). ``` Here we actually create everything using the Signature. So we don't @@ -315,14 +315,14 @@ the `add` function. ```erlang time_direct_vs_signature_dict() -> - io:format("Timing dict~n"), - Dict = create_dict(), - test_avg(fun() -> - dict:size(dict:store(some_key, some_value, Dict)) - end, - 1000000), - io:format("Timing ec_dict implementation of ec_dictionary~n"), - time_dict_type(ec_dict). + io:format("Timing dict~n"), + Dict = create_dict(), + test_avg(fun() -> + dict:size(dict:store(some_key, some_value, Dict)) + end, + 1000000), + io:format("Timing ec_dict implementation of ec_dictionary~n"), + time_dict_type(ec_dict). ``` The `test_avg` function runs the provided function the number of times @@ -337,12 +337,12 @@ implementations. Lets take a look at the `time_dict_type` function. ```erlang time_dict_type(Type) -> - io:format("Testing ~p~n", [Type]), - Dict = create_dictionary(Type), - test_avg(fun() -> - ec_dictionary:size(ec_dictionary:add(some_key, some_value, Dict)) - end, - 1000000). + io:format("Testing ~p~n", [Type]), + Dict = create_dictionary(Type), + test_avg(fun() -> + ec_dictionary:size(ec_dictionary:add(some_key, some_value, Dict)) + end, + 1000000). ``` As you can see we take the type as an argument (we need it for `dict` @@ -389,10 +389,10 @@ to create a creation helper just like we did for dict. ```erlang create_rbdict() -> - lists:foldl(fun(El, Dict) -> - ec_rbdict:add(El, El, Dict) - end, ec_rbdict:new(), - lists:seq(1,100)). + lists:foldl(fun(El, Dict) -> + ec_rbdict:add(El, El, Dict) + end, ec_rbdict:new(), + lists:seq(1,100)). ``` This is exactly the same as `create_dict` with the exception that dict @@ -404,14 +404,14 @@ implementation, but we don't for the `ec_dictionary` test. ```erlang time_direct_vs_signature_rbdict() -> - io:format("Timing rbdict~n"), - Dict = create_rbdict(), - test_avg(fun() -> - ec_rbdict:size(ec_rbdict:add(some_key, some_value, Dict)) - end, - 1000000), - io:format("Timing ec_dict implementation of ec_dictionary~n"), - time_dict_type(ec_rbdict). + io:format("Timing rbdict~n"), + Dict = create_rbdict(), + test_avg(fun() -> + ec_rbdict:size(ec_rbdict:add(some_key, some_value, Dict)) + end, + 1000000), + io:format("Timing ec_dict implementation of ec_dictionary~n"), + time_dict_type(ec_rbdict). ``` And there we have our test. What do the results look like? From 6e9c1b0a22064d52231e65064d91d748a025e4d5 Mon Sep 17 00:00:00 2001 From: Ariel Date: Mon, 18 Dec 2023 21:26:06 +0100 Subject: [PATCH 7/7] Another typo in math mode --- doc/signatures.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/signatures.md b/doc/signatures.md index 4ad716f..cc5fd32 100644 --- a/doc/signatures.md +++ b/doc/signatures.md @@ -23,7 +23,7 @@ comparison for successful lookup, and a table of size $n$ with chaining and $k$ keys has the minimum $\max(0, k-n)$ collisions and $\mathcal{O}(1 + k/n)$ comparisons for lookup. While for skip lists the performance characteristics are about as good as that of randomly-built binary -search trees - namely ($\mathcal{O}(\log n)$). So the choice of which to select +search trees - namely $\mathcal{O}(\log n)$. So the choice of which to select depends very much on memory available, insert/read characteristics, etc. So delegating the choice to a single point in your code is a very good idea. Unfortunately, in Erlang that's so easy to do at the moment.