On Friday, January 11, 2019 at 5:12:29 AM UTC-7, Albert van der Horst wrote:
> What is the purpose of publishing this code?
> Do you hope that someone else will benchmark this to compare it
> to other sorts?
> And then you deny the correctness, because the results don't please you?
You insulted the entire world by using the word "canonical" to describe
your code, implying that your code is a gift from God.
Whenever the ANS-Forth cult puffs up their hollow chests with arrogance,
I make it my business to deflate them.
I upgraded my code to make it faster. These are the results of several runs:
50000 element liked-list was initially sorted:
randomly forward reverse
15ms 16ms 31ms
16ms 31ms 16ms
31ms 15ms 32ms
31ms 16ms 15ms
15ms 16ms 31ms
32ms 15ms 16ms
16ms 15ms 16ms
15ms 32ms 15ms
This is just my laptop running Windows.
I have no idea how this compares to Doug Hoffman's computer in speed.
On Monday, December 17, 2018 at 4:11:15 AM UTC-7, Doug Hoffman wrote:
> I created a ll of 50,000 random integers. Your
> code sorted that in 0.016 seconds on my rather
> slow machine. Impressive! Nice work.
>
> Yes, I think your mergesort could (should) be part
> of anyone's library.
I think that Doug Hoffman's claim of 16ms is not true.
This is a typical example of one ANS-Forth cult member supporting another.
There is a lot of variance in the time required from one run to the next.
The fact that he provided only one number, 16ms, indicates to me that
he cherry-picked your best result pretending that it is an average result.
He may have totally fabricated the 16ms result. There is no source-code!
I am presumably doing fewer comparisons because I have SORT-FOUR.
The comparison is so fast (comparing an integer) that the number of
comparisons has less of an effect than it would in a real-life application
that would presumably have a more complicated comparison.
You refuse to provide source-code in ANS-Forth for a function to sort
a general-purpose linked-list. Yet you want your code to be in a library!
This refusal implies that the 16ms result was not true.
You are afraid to benchmark your "canonical" code against mine.
I don't think the ANS-Forth cult should ever be allowed to succeed at anything!
In order for Forth to succeed, the ANS-Forth cult must first be discredited.
ANS-Forth is the reason why Forth lost popularity in the real world.
The following is my code.
This is not "canonical" of course --- it can be improved yet more.
--------------------------------------------------------------------------
\ ******
\ ****** The following are for sorting lists.
\ ******
\ char & comment \ test code for SORT-LIST
list
w field .num
constant num
: init-num ( n node -- node )
init-list >r
r@ .num !
r> ;
: new-num ( n -- node )
num alloc
init-num ;
: >>num ( head n -- new-head )
new-num link ;
: <rnd-num> ( how-many range seed -- head ) \ SEED can be any nonzero number
seed !
locals| range |
nil swap 0 ?do
range rnd >>num loop ;
: rnd-num ( how-many -- head )
1000000 1 <rnd-num> ;
: kill-num ( head -- )
each[ dealloc ]each ;
: show-num ( head -- )
each[ .num @ 7 .r ]each ;
&
macro: one-link ( 1stHead 2ndHead -- NewHead ) \ like LINK except requires that the 1stHead list has exactly 1 node
over .fore ! ;
\ The 1stHead doesn't need INIT-LIST done to it. If it has garbage in the .FORE field that will get over-written anyway.
macro: prepend-list ( head build -- new-head )
begin over while \ -- head build
over .fore @ -rot \ -- rest head build
one-link \ -- rest build
repeat nip ;
: reverse ( head -- new-head ) \ reverses the order of all the nodes
nil prepend-list ;
\ The hand-coded REVERSE is more efficient than the obvious way: nil swap each[ swap one-link ]each ;
macro: tail-reverse ( head -- new-tail new-head ) \ like REVERSE except also provides the tail node
dup \ -- tail head \ our current HEAD will be the TAIL afterward
nil prepend-list ; \ -- tail new-head
\ comparer: i*x new-node node -- j*x new-node flag \ insert new-node prior to node?
: insert-ordered ( head 'comparer node -- new-head 'comparer )
init-list rover rover \ -- head 'comparer node head 'comparer
find-prior \ -- head 'comparer node -1|prior|false
dup 0= if drop rot swap link swap exit then \ append
dup -1 = if drop rot one-link swap exit then \ prepend
insert ;
: <sort-list> ( head 'comparer -- new-head )
nil swap rot each[ insert-ordered ]each drop ;
\ <SORT-LIST> used to be our SORT-LIST --- INSERT-ORDERED is still a good choice if you need a list continuously sorted.
\ If the comparer provides a TRUE on node > new-node, then the list will be sorted in ascending order.
\ The following is a typical comparer function:
\ : int> ( new-node node -- new-node flag ) \ assumes that we are sorting on a W field called .N
\ .n @ over .n @ u> ;
\ <SORT-LIST> does an InsertionSort. SORT-LIST does a MergeSort which has fewer comparisons and hence is theoretically faster.
macro: <fast-delink> ( head last -- head rest ) \ requires that we have 1 or more nodes in the HEAD list
dup .fore @ \ -- head 2nd rest
nil rot .fore ! ;
macro: fast-delink-one ( head -- head rest ) \ requires that we have 1 or more nodes in the HEAD list
dup .fore @ \ -- head rest
nil rover .fore ! ;
macro: fast-delink-two ( head -- head rest ) \ requires that we have 2 or more nodes in the HEAD list
dup .fore @ \ -- head 2nd
<fast-delink> ;
macro: fast-delink-three ( head -- head rest ) \ requires that we have 3 or more nodes in the HEAD list
dup .fore @ .fore @ \ -- head 3rd
<fast-delink> ;
macro: fast-delink-four ( head -- head rest ) \ requires that we have 4 or more nodes in the HEAD list
dup .fore @ .fore @ .fore @ \ -- head 4th
<fast-delink> ;
macro: append-list ( tail head build -- build ) \ the TAIL is of the BUILD list
-rot \ -- build tail head
swap .fore ! ; \ -- build \ append HEAD to TAIL
\ APPEND-LIST will never have a BUILD being NIL because at least one node will already have been appended.
: merge-sorted-lists ( headA headB 'comparer -- head )
>r nil >r \ -- headA headB \ return: -- 'comparer build
begin \ -- tail A B \ the TAIL node is not there until the second pass
over 0= if nip r> append-list rdrop exit then \ if A is done, append B and exit (won't happen first pass)
dup 0= if drop r> append-list rdrop exit then \ if B is done, append A and exit (won't happen first pass)
1 comparisons +!
2dup rr@ execute nip \ -- A B A<B?
if swap then \ -- big sml \ we need to append the small node
r@ if \ -- tail big sml \ we have a TAIL from last time we appended
swap >r dup >r \ -- tail sml \ return: -- 'comparer build big sml
fast-delink-one -rot \ -- rest tail sml \ delink head node from rest of list
swap .fore ! \ -- rest \ append SML to TAIL
r> swap r> \ -- sml rest big \ SML is our new TAIL, REST is A and BIG is B
else \ -- big sml \ we don't have a TAIL yet because this is the first pass
tuck rdrop \ -- sml big sml \ return: -- 'comparer \ rdrop BUILD that was NIL
fast-delink-one swap >r \ -- sml big rest \ return: -- 'comparer build \ SML head is our new BUILD
then
again ;
\ There is no case in which MERGE-SORTED-LISTS is called with two either HEADA or HEADB being NIL nodes. We go through the loop at least once.
\ At least one list will have something at the start, so there is no case in which we return a NIL node.
\ The first time through the loop, there is no TAIL node on the data-stack. After that, we have a TAIL node which is the last node we appended.
\ By the time APPEND-LIST executes we are guaranteed to have a TAIL node because we have been through the loop at least once.
\ Instead of SORT-FOUR I had previously just used <SORT-LIST> for the small-lists.
\ A small-list of 4 elements is okay because SORT-FOUR can be used. SORT-FOUR is much faster than <SORT-LIST> is.
\ A larger small-list will save memory, but a function like SORT-FOUR is unrealistic because it would be too big and complicated.
\ For a larger small-list, <SORT-LIST> would need to be used. The larger that the small-list is, the slower SORT-LIST is.
\ The following need to be LATE-MACRO: because they use 'COMPARE internally, which is a SORT-LIST local.
late-macro: node<? ( nodeA nodeB -- flag ) \ used by SORT-FOUR etc.
1 comparisons +!
'comparer execute \ -- A A<B?
nip ;
late-macro: node>? ( nodeA nodeB -- flag ) \ used by SORT-FOUR etc.
1 comparisons +!
'comparer execute 0= \ -- A A>B?
nip ;
late-macro: sort-two ( head -- new-head ) \ used by SORT-LIST \ assumes the list is exactly 2 nodes in length
fast-delink-one \ -- B A
2dup node>? if swap then \ -- A B \ we know now that A < B
one-link ;
late-macro: sort-three ( head -- new-head ) \ used by SORT-LIST \ assumes the list is exactly 3 nodes in length
fast-delink-one fast-delink-one \ -- C B A
>r \ -- C B \ return: -- A
2dup node>? if swap then \ -- B C \ return: -- A \ we know now that B < C
dup r@ node<? if \ we know now that C < A
r> \ -- B C A
else \ we know now that A < C
over r@ node<? if \ -- B C \ return: -- A \ we know now that B < A
r> swap \ -- B A C
else \ we know now that A < B
r> -rot \ -- A B C \ we already know that B < C
then
then
one-link one-link ;
late-macro: sort-four ( head -- new-head ) \ used by SORT-LIST \ assumes the list is exactly 4 nodes in length
fast-delink-one fast-delink-one fast-delink-one \ -- D C B A
2dup node<? if swap then \ -- D C B A \ we know now that A < B
2swap \ -- B A D C
2dup node<? if swap then \ -- B A D C \ we know now that C < D
swap >r rot >r \ -- A C \ return: -- D B \ A and C are both small, so one of them must be 1st
2dup node<? if \ A < C so A is 1st
r> \ -- A C B \ return: -- D
2dup node<? if \ C < B so C is 2nd
r> \ -- A C B D
2dup node>? if swap then \ -- 1st 2nd 3rd 4th \ B or D the greater becomes 3rd
else \ B < C so B is 2nd
swap r> \ -- A B C D \ we already know C < D
then
else \ C < A so C is 1st
swap \ -- C A \ return: -- D B
r> r> \ -- C A B D \ B and D are both big, so one of them must be 4th
2dup node>? if \ B > D so B is 4th (else D is 4th and we are done)
swap >r \ -- C A D \ return: -- B
2dup node>? if swap then \ -- C 2nd 3rd \ return: -- B \ A or D the greater becomes 3rd
r> \ -- 1st 2nd 3rd 4th
then
then
one-link one-link one-link ;
\ SORT-FOUR does 112 comparisons for the 24 possibilities.
\ I also have a version that does 120 comparisons but is faster for data that is mostly presorted.
macro: sort-small-list ( 0 sorted-list rest -- 0 new-sorted-list new-rest ) \ REST is not NIL \ NEW-REST may be NIL if we are done
fast-delink-four swap sort-four swap ; \ delink a four-list and sort it \ the top list is unsorted and possibly NIL
: sort-list { head 'comparer -- new-head } \ iterative; with SORT-FOUR used on lists of size 4
head 0= if head exit then
0 \ -- 0 \ sentinel
head dup length 3 and \ -- 0 head partial-size \ the partial list is [0,3] in length
case \ special-case the partial list because SORT-FOUR won't work
1 of fast-delink-one endof
2 of fast-delink-two swap sort-two swap endof
3 of fast-delink-three swap sort-three swap endof
endcase \ -- 0 sorted-list rest \ REST may be NIL if the length is [0,3]
begin dup while \ the top list length is a multiple of 4 and any list underneath is sorted
sort-small-list \ sort the next one
dup if sort-small-list >r 'comparer merge-sorted-lists r> \ sort and merge the next one for a list of length 8
dup if sort-small-list >r 'comparer merge-sorted-lists r> then \ sort and merge the next one for a list of length 12
then
repeat drop \ -- 0 sorted-lists... \ drop the top list which is NIL now
begin over while \ assumes at least one sorted-list
0 >r \ sentinel
begin dup while
over if 'comparer merge-sorted-lists then
>r
dup if over if \ this helps to reduce memory usage
'comparer merge-sorted-lists r> 'comparer merge-sorted-lists >r then then
repeat drop \ --
0 \ sentinel
begin r> dup while
r@ if r> 'comparer merge-sorted-lists then
repeat drop
repeat \ -- 0 head
nip ; \ -- head
\ The first loop in SORT-LIST previously just did SORT-SMALL-LIST and that was it, so all the sorted lists were length 4.
\ Now we do one, two or three small lists, for a list of 4, 8 or 12 (most will be 12, but at the end you may get a list of 4 or 8).
\ The advantage is that fewer elements go onto the data stack, so there should be less data-cache thrashing.
\ Also, there is less chance of data-stack or return-stack overflow when sorting a gigantic list.
\ The disadvantage is that we are merging lists of length 8 and 4 which is less efficient than merging same-size lists.
\ When we get into the second loop, all of the lists are length 12 (except possibly the last of length 4 or 8).
\ So, in the second loop we are merging same-size lists, and the second loop is the most time-consuming so this is where efficiency matters.
\ The first loop in the second loop (data-stack to return-stack) also does four lists rather than two lists to reduce memory usage.
\ char & comment \ test code for SORT-LIST
: num> ( new-node node -- new-node flag )
.num @ over .num @ u> ;
: sort-num ( head -- new-head )
['] num> sort-list ;
: slow-sort-num ( head -- new-head )
['] num> <sort-list> ;
: num-sorted? { head | bad? -- good? } \ assumes NUM list filled with non-negative numbers
-1
head each[ .num @ tuck > if true to bad? then ]each
drop
bad? 0= ;
: test-sort-num-four ( -- )
nil 1 >>num 2 >>num 3 >>num 4 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 1 >>num 2 >>num 4 >>num 3 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 1 >>num 3 >>num 2 >>num 4 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 1 >>num 3 >>num 4 >>num 2 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 1 >>num 4 >>num 2 >>num 3 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 1 >>num 4 >>num 3 >>num 2 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 2 >>num 1 >>num 3 >>num 4 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 2 >>num 1 >>num 4 >>num 3 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 2 >>num 3 >>num 1 >>num 4 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 2 >>num 3 >>num 4 >>num 1 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 2 >>num 4 >>num 1 >>num 3 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 2 >>num 4 >>num 3 >>num 1 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 3 >>num 1 >>num 2 >>num 4 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 3 >>num 1 >>num 4 >>num 2 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 3 >>num 2 >>num 1 >>num 4 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 3 >>num 2 >>num 4 >>num 1 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 3 >>num 4 >>num 1 >>num 2 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 3 >>num 4 >>num 2 >>num 1 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 4 >>num 1 >>num 2 >>num 3 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 4 >>num 1 >>num 3 >>num 2 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 4 >>num 2 >>num 1 >>num 3 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 4 >>num 2 >>num 3 >>num 1 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 4 >>num 3 >>num 1 >>num 2 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
nil 4 >>num 3 >>num 2 >>num 1 >>num cr dup show-num ." >>> " sort-num dup show-num kill-num
;
&
VFX? [if]
: check-result ( head -- head )
cr dup num-sorted? if ." sort good" else ." sort bad" then
." comparisons done: " comparisons @ .
comparisons off ;
: <time-sort-num> ( list-size 'sort -- )
>r
0 comparisons !
ticks swap rnd-num ticks rot - cr ." MS to generate list: " .
comparisons off
ticks swap r@ execute ticks rot - cr ." MS to sort random: " .
check-result
ticks swap r@ execute ticks rot - cr ." MS to sort forward-sorted: " .
check-result
reverse
ticks swap r@ execute ticks rot - cr ." MS to sort reverse-sorted: " .
check-result
kill-num rdrop ;
: time-sort-num ( list-size -- )
['] sort-num <time-sort-num> ;
: time-slow-sort-num ( list-size -- )
['] slow-sort-num <time-sort-num> ;
[then]
--------------------------------------------------------------------------