Difference between revisions of "Talk:LlListSort"
Pedro Oval (talk | contribs) (→Bubble Sort?: Winner) |
Pedro Oval (talk | contribs) m (<cpp> to <source>) |
||
(14 intermediate revisions by 4 users not shown) | |||
Line 2: | Line 2: | ||
This script: | This script: | ||
< | <source lang="lsl2"> | ||
default | default | ||
{ | { | ||
Line 10: | Line 10: | ||
} | } | ||
} | } | ||
</ | </source> | ||
outputs | outputs | ||
Line 43: | Line 43: | ||
::::Same difference, as Boost uses std::sort: [http://svn.boost.org/svn/boost/trunk/boost/range/algorithm/sort.hpp]. Digging a bit I have found this: [http://en.wikipedia.org/wiki/Introsort#Implementations Introsort#Implementations (Wikipedia)] which made it sound like any attempt at reproducing the results without GNU libstdc++ was going to be too painful to be worth it. Then I've tried with the C++11 program below; it was sorted in a stable way, as the result is: "1,3, 1,4, 1,9, 3,2, 3,4, 3,3, 3,6, ". I'm willing to make more experiments but I'm out of ideas at the moment. Any suggestions? (P.S.: who chose the colors for the C++ highlighter? Yuck!) --[[User:Pedro Oval|Pedro Oval]] 09:30, 9 April 2014 (PDT) | ::::Same difference, as Boost uses std::sort: [http://svn.boost.org/svn/boost/trunk/boost/range/algorithm/sort.hpp]. Digging a bit I have found this: [http://en.wikipedia.org/wiki/Introsort#Implementations Introsort#Implementations (Wikipedia)] which made it sound like any attempt at reproducing the results without GNU libstdc++ was going to be too painful to be worth it. Then I've tried with the C++11 program below; it was sorted in a stable way, as the result is: "1,3, 1,4, 1,9, 3,2, 3,4, 3,3, 3,6, ". I'm willing to make more experiments but I'm out of ideas at the moment. Any suggestions? (P.S.: who chose the colors for the C++ highlighter? Yuck!) --[[User:Pedro Oval|Pedro Oval]] 09:30, 9 April 2014 (PDT) | ||
<cpp> | <source lang="cpp"> | ||
#include <iostream> | #include <iostream> | ||
#include <algorithm> | #include <algorithm> | ||
Line 65: | Line 65: | ||
return 0; | return 0; | ||
} | } | ||
</ | </source> | ||
:tried substituting partial_sort (I think that bypasses the insertion sort libstdc++ likes to use on small sets), almost the same as LSL but not close enough. 1,4, 1,3, 1,9, 3,3, 3,2, 3,4, 3,6, --[[User:ObviousAltIsObvious Resident|ObviousAltIsObvious Resident]] 15:34, 9 April 2014 (PDT) | :tried substituting partial_sort (I think that bypasses the insertion sort libstdc++ likes to use on small sets), almost the same as LSL but not close enough. 1,4, 1,3, 1,9, 3,3, 3,2, 3,4, 3,6, --[[User:ObviousAltIsObvious Resident|ObviousAltIsObvious Resident]] 15:34, 9 April 2014 (PDT) | ||
Line 102: | Line 102: | ||
I'm still researching, but I'll advance some information. I've reproduced the results with this script, which implements a very simple sort algorithm that is not listed in Wikipedia: | I'm still researching, but I'll advance some information. I've reproduced the results with this script, which implements a very simple sort algorithm that is not listed in Wikipedia: | ||
< | <source lang="lsl2"> | ||
list Sort(list input, integer stride) | list Sort(list input, integer stride) | ||
{ | { | ||
Line 140: | Line 140: | ||
// [09:50:20] Sort: N: 1, 3, 1, 4, 1, 9, 3, 3, 3, 2, 3, 4, 3, 6 | // [09:50:20] Sort: N: 1, 3, 1, 4, 1, 9, 3, 3, 3, 2, 3, 4, 3, 6 | ||
// [09:50:20] Sort: Equal | // [09:50:20] Sort: Equal | ||
</ | </source> | ||
I realized it was a candidate while trying to understand how bubble sort would preserve the ordering as claimed by the OpenSim code. I couldn't, so I looked into the code in more detail, and I realized that the sorting algorithm used was ''not'' bubble sort, but the one implemented in the above script. It's a naïve algorithm similar to selection sort, but less optimal: it goes through each element, and does a sweep over the rest of the elements to see if there's one less than it. If so, it swaps them (selection sort just takes note of the index position of the least element, and swaps them at the end of the inner loop instead of as it goes). At the end of each sweep, the element in question will have the least of the remaining elements in the list. It's still O(n²), and worse in performance than selection sort (since it does more swaps), but it does indeed guarantee preservation of the order of the types as claimed, assuming that comparison of two elements of different types does not cause a swap. It's also not stable, by its nature, and as proved by the results above. --[[User:Pedro Oval|Pedro Oval]] 10:09, 19 April 2014 (PDT) | I realized it was a candidate while trying to understand how bubble sort would preserve the ordering as claimed by the OpenSim code. I couldn't, so I looked into the code in more detail, and I realized that the sorting algorithm used was ''not'' bubble sort, but the one implemented in the above script. It's a naïve algorithm similar to selection sort, but less optimal: it goes through each element, and does a sweep over the rest of the elements to see if there's one less than it. If so, it swaps them (selection sort just takes note of the index position of the least element, and swaps them at the end of the inner loop instead of as it goes). At the end of each sweep, the element in question will have the least of the remaining elements in the list. It's still O(n²), and worse in performance than selection sort (since it does more swaps), but it does indeed guarantee preservation of the order of the types as claimed, assuming that comparison of two elements of different types does not cause a swap. It's also not stable, by its nature, and as proved by the results above. --[[User:Pedro Oval|Pedro Oval]] 10:09, 19 April 2014 (PDT) | ||
'''Update:''' Selection Sort fails to reproduce the results, which is expected. However, the above algorithm matched the results every time I've tried. I tested with this list generation code in place of the constant: | '''Update:''' Selection Sort fails to reproduce the results, which is expected. However, the above algorithm matched the results every time I've tried. I tested with this list generation code in place of the constant: | ||
< | <source lang="lsl2"> | ||
list MyList; | list MyList; | ||
while (llGetListLength(MyList) < 500) | while (llGetListLength(MyList) < 500) | ||
Line 151: | Line 151: | ||
MyList = MyList + (integer)llFrand(12) + llGetListLength(MyList); | MyList = MyList + (integer)llFrand(12) + llGetListLength(MyList); | ||
} | } | ||
</ | </source> | ||
It seems we have a winner. --[[User:Pedro Oval|Pedro Oval]] 10:51, 19 April 2014 (PDT) | It seems we have a winner. --[[User:Pedro Oval|Pedro Oval]] 10:51, 19 April 2014 (PDT) | ||
'''Update 2:''' It gets better. Type feathering is NOT respected in case of a descending sort. OpenSim gives different results to SL in that case. It looks like type feathering is not a feature, but a side effect of the algorithm used. I've reproduced the results by always swapping when the types are different. The results I got are consistent with this: the comparison function returns TRUE if A and B are of the same type and A is greater than B, and FALSE otherwise, and the swap is done if (the comparison function returns TRUE) '''xor''' (ascending!=1), causing it to ''always'' swap elements of different types for descending sort, thus destroying the feathering. OpenSim respects the feathering for descending order. --[[User:Pedro Oval|Pedro Oval]] 14:38, 19 April 2014 (PDT) | |||
---- | |||
One sure thing : llListSort is not O(N*N) . LLstsort is O(N * Log2(N) ) | |||
Here some results of your algorithm ( 2nd column = number of pairs ; 3rd column time in frames per iteration) | |||
BubbleSort | |||
NumberTest N size Average Time in Frames | |||
Test #1 with 2 0.020000 | |||
Test #2 with 3 0.050000 | |||
Test #3 with 4 0.090000 | |||
Test #4 with 6 0.200000 | |||
Test #5 with 8 0.200000 | |||
Test #6 with 12 0.650000 | |||
Test #7 with 16 0.970000 | |||
Test #8 with 24 1.850000 | |||
Test #9 with 32 2.300000 | |||
Test #10 with 48 5.590000 | |||
Test #11 with 64 7.540000 | |||
Test #12 with 96 18.270000 | |||
Test #13 with 128 28.950000 | |||
Test #14 with 160 53.440000 | |||
Test #15 with 192 71.920000 | |||
Test #16 with 224 90.050000 | |||
Test #17 with 256 112.140000 | |||
Time ( 2*128 ) = (2*2) * Time(128) => ALgorithm is O(N*N) | |||
And with LListSort | |||
LListSort | |||
Test #1 with 2 0.040000 | |||
Test #2 with 3 0.070000 | |||
Test #3 with 4 0.040000 | |||
Test #4 with 6 0.050000 | |||
Test #5 with 8 0.070000 | |||
Test #6 with 12 0.150000 | |||
Test #7 with 16 0.080000 | |||
Test #8 with 24 0.210000 | |||
Test #9 with 32 0.320000 | |||
Test #10 with 48 0.340000 | |||
Test #11 with 64 0.430000 | |||
Test #12 with 96 0.280000 | |||
Test #13 with 128 0.670000 | |||
Test #14 with 160 0.650000 | |||
Test #15 with 192 0.760000 | |||
Test #16 with 224 0.660000 | |||
Test #17 with 256 0.840000 | |||
Time ( 2*128 ) = 2*log(2) * Time(128) => ALgorithm is O(N*LogN) | |||
<source lang="lsl2"> | |||
list Sort(list input, integer stride) | |||
{ | |||
integer i; | |||
integer j; | |||
for (i = 0; i < llGetListLength(input) - stride; i += stride) | |||
{ | |||
for (j = i + stride; j < llGetListLength(input); j += stride) | |||
{ | |||
if (llList2Integer(input, i) > llList2Integer(input, j)) | |||
{ | |||
// Swap records i & j | |||
input = llListReplaceList( | |||
llListReplaceList(input, llList2List(input, j, j+stride-1), i, i+stride-1), | |||
llList2List(input, i, i+stride-1), j, j+stride-1); | |||
} | |||
} | |||
} | |||
return input; | |||
} | |||
default { | |||
state_entry() { | |||
list tests = [ | |||
// Ascending lists | |||
2, TRUE, | |||
3, TRUE, | |||
4, TRUE, | |||
6, TRUE, | |||
8, TRUE, | |||
12, TRUE, | |||
16, TRUE, | |||
24, TRUE, | |||
32, TRUE, | |||
48, TRUE, | |||
64, TRUE, | |||
96, TRUE, | |||
128, TRUE, | |||
160, TRUE, | |||
192, TRUE, | |||
224, TRUE, | |||
256, TRUE | |||
/* 320, TRUE, | |||
384, TRUE, | |||
448, TRUE, | |||
512, TRUE | |||
*/ | |||
]; | |||
integer repetitions = 100; | |||
integer test = 0; integer testsLength = tests != []; | |||
integer listSize; integer ascending; integer i; list t; | |||
float f; string s; list l = []; | |||
float nFrameStart; | |||
float nFrameEnd; | |||
float overhead; | |||
llOwnerSay("Starting tests…"); | |||
for (; test < testsLength; test += 2) { | |||
listSize = llList2Integer(tests, test); | |||
ascending = llList2Integer(tests, test + 1); | |||
if (ascending) { | |||
i = 0; | |||
for (; i < listSize; ++i) | |||
l += [(integer)llFrand(3),llFrand(10000)] ; | |||
} else { | |||
i = listSize; | |||
while ((--i) >= 0) | |||
l += (integer)llFrand(10000); | |||
} | |||
i = 0; | |||
nFrameStart = (float)llGetEnv("frame_number"); | |||
for (; i < repetitions; ++i) | |||
t = []; | |||
nFrameEnd = (float)llGetEnv("frame_number"); | |||
overhead = nFrameEnd - nFrameStart; | |||
i = 0; | |||
nFrameStart = (float)llGetEnv("frame_number"); | |||
for (; i < repetitions; ++i) | |||
// t = llListSort(l, 2, ascending); | |||
t = Sort(l,2); | |||
nFrameEnd = (float)llGetEnv("frame_number"); | |||
f = nFrameEnd - nFrameStart - overhead; | |||
t = l = []; | |||
f /= (float)repetitions; | |||
s = "\tTest #" + (string)((test / 2) + 1) + " with\t" + (string)listSize + "\t" + (string)( f ); | |||
llOwnerSay(s); | |||
} | |||
llOwnerSay("Tests complete."); | |||
} | |||
} | |||
</source> | |||
--[[User:Miranda Umino|Miranda Umino]] 21:41, 19 April 2014 (PDT) | |||
:It's definitively quadratic, i.e. O(n²). http://inky.ws/g/2zz --[[User:Pedro Oval|Pedro Oval]] 19:39, 5 May 2014 (PDT) | |||
:'''Update:''' Added benchmarking script to the linked gallery. --[[User:Pedro Oval|Pedro Oval]] 06:15, 6 May 2014 (PDT) | |||
:'''Update 2:''' I tried the same benchmarking script listed in Inky under LSO and the strangest thing happened. I changed the list addition line to use the <code>(MyList = []) + MyList</code> trick, which allowed for an extra batch of 50. Here are the results I got: | |||
:{| {{Prettytable}} | |||
|- {{Hl2}} | |||
!N !! Time | |||
|- | |||
|50 || 3.333132 | |||
|- | |||
|100 || 6.786429 | |||
|- | |||
|150 || 13.334011 | |||
|- | |||
|200 || 13.333488 | |||
|- | |||
|250 || 13.333256 | |||
|- | |||
|300 || 13.333005 | |||
|- | |||
|350 || 13.332855 | |||
|- | |||
|400 || 13.334793 | |||
|- | |||
|450 || 13.334989 | |||
|- | |||
|500 || 13.332029 | |||
|} | |||
:Looks like under LSO the function is being throttled. --[[User:Pedro Oval|Pedro Oval]] 13:36, 6 May 2014 (PDT) | |||
:'''Update 3:''' It seems it's throttled under Mono as well, just not so much. I've added to the [http://inky.ws/g/2zz gallery] a graphic with a benchmark over the first 200-300 values, going through them one at a time. The delay seems roughly linear on N up to about N=80. Then it gets constant (to almost 3 seconds, actually about 2.95 or 2.97, according to the data, meaning almost 10 ms per call) until the actual time the function takes overtakes the throttle. When does that happen, depends on what is being compared (I used integers and vectors because they have a noticeably different comparison time) and on the performance of the sim (green line was taken on a faster sim). | |||
:The benchmarking script is the same, just setting Step=1 and MaxN=300. The vectors were constant, set to <1,2,3>. For the descending test, I just changed all TRUE to FALSE. The first run is up to N=200 only because I thought it was enough, but when I finished and graphed it, it wasn't clear enough from the results I got, so I raised it to 300. | |||
:I conjecture that the spikes are GC kicking in and may depend on available memory. The sim I went to to take the sample with the green line was much emptier. --[[User:Pedro Oval|Pedro Oval]] 05:07, 19 May 2014 (PDT) | |||
== Merge Sort in LSL == | |||
I've written a Merge Sort that's nearly comparable to the llListSort provided by LSL. It supports strided lists (as in the example below), but cannot reverse the order as llListSort does. I ran this test a few times on it, and surprisingly, it's faster than llListSort. The code: | |||
<source lang="lsl2">list elements = [8.627350, "a3c6cf64-8daf-b6ca-9851-8a49a597fafc", 18.943806, "3e0a230a-16ef-f539-b070-0ef0a16b1835", 6.843595, "aa3183b2-a5ad-abb3-98c2-c07517c049b5", 5.199630, "513ec659-a280-5ea4-ab3c-5f3a5544820e", 14.510715, "7f1f52bc-c63f-aef0-73c9-cd97f1bddccc", 12.230530, "3c719f4f-072d-b35f-9fe6-ed157e06c487", 3.188807, "a84db094-9630-79fc-eb02-74bcbd030190", 1.648486, "e0a8af06-2c43-57df-9571-22c035424acc", 8.481318, "891c3eba-3723-29c4-f1ea-032b6dad2b1b", 17.444149, "e4b04775-8d68-0d4a-773f-19f02c112066"]; | |||
list merge(list left, list right, integer stride) | |||
{ | |||
list result; | |||
while(llGetListLength(left) > 0 || llGetListLength(right) > 0) | |||
{ | |||
if(llGetListLength(left) > 0 && llGetListLength(right) > 0) | |||
{ | |||
// Ensure we're comparing the same things... | |||
if(llGetListEntryType(left, 0) == llGetListEntryType(right, 0)) | |||
{ | |||
// Get values in a re-cast-able form | |||
integer type = llGetListEntryType(left, 0); | |||
string InitialLeftValue = llList2String(left, 0); | |||
string InitialRightValue = llList2String(right, 0); | |||
// Handle all type cases | |||
if(type == TYPE_INTEGER) | |||
{ | |||
integer leftValue = (integer)InitialLeftValue; | |||
integer rightValue = (integer)InitialRightValue; | |||
if(leftValue <= rightValue) | |||
{ | |||
result += llList2List(left, 0, 1); | |||
left = llDeleteSubList(left, 0, stride - 1); | |||
} | |||
else | |||
{ | |||
result += llList2List(right, 0, 1); | |||
right = llDeleteSubList(right, 0, stride - 1); | |||
} | |||
} | |||
else if(type == TYPE_FLOAT) | |||
{ | |||
float leftValue = (float)InitialLeftValue; | |||
float rightValue = (float)InitialRightValue; | |||
if(leftValue <= rightValue) | |||
{ | |||
result += llList2List(left, 0, 1); | |||
left = llDeleteSubList(left, 0, stride - 1); | |||
} | |||
else | |||
{ | |||
result += llList2List(right, 0, 1); | |||
right = llDeleteSubList(right, 0, stride - 1); | |||
} | |||
} | |||
else if(type == TYPE_VECTOR) | |||
{ | |||
float leftValue = llVecMag((vector)InitialLeftValue); | |||
float rightValue = llVecMag((vector)InitialRightValue); | |||
if(leftValue <= rightValue) | |||
{ | |||
result += llList2List(left, 0, 1); | |||
left = llDeleteSubList(left, 0, stride - 1); | |||
} | |||
else | |||
{ | |||
result += llList2List(right, 0, 1); | |||
right = llDeleteSubList(right, 0, stride - 1); | |||
} | |||
} | |||
else if(type == TYPE_ROTATION) | |||
{ | |||
float leftValue = llRot2Angle((rotation)InitialLeftValue); | |||
float rightValue = llRot2Angle((rotation)InitialRightValue); | |||
if(leftValue <= rightValue) | |||
{ | |||
result += llList2List(left, 0, 1); | |||
left = llDeleteSubList(left, 0, stride - 1); | |||
} | |||
else | |||
{ | |||
result += llList2List(right, 0, 1); | |||
right = llDeleteSubList(right, 0, stride - 1); | |||
} | |||
} | |||
} | |||
} | |||
else if(llGetListLength(left) > 0) | |||
{ | |||
result += llList2List(left, 0, 1); | |||
left = llDeleteSubList(left, 0, stride - 1); | |||
} | |||
else if(llGetListLength(right) > 0) | |||
{ | |||
result += llList2List(right, 0, 1); | |||
right = llDeleteSubList(right, 0, stride - 1); | |||
} | |||
} | |||
return result; | |||
} | |||
list llListSortFast(list in, integer stride) | |||
{ | |||
if(llGetListLength(in) <= stride) | |||
{ | |||
return in; | |||
} | |||
integer middle = (llGetListLength(in) / stride) / 2; | |||
list left = llList2List(in, 0, (middle) * stride - 1); | |||
list right = llList2List(in, middle * stride, -1); | |||
left = llListSortFast(left, stride); | |||
right = llListSortFast(right, stride); | |||
return merge(left, right, stride); | |||
} | |||
default | |||
{ | |||
state_entry() | |||
{ | |||
llSay(0, "Input: \n Merge Sort:" + llList2CSV(elements)); | |||
float time = llGetAndResetTime(); | |||
llSay(0, "Sorted Result: \n" + llList2CSV(llListSortFast(elements, 2))); | |||
llSay(0, "Time Taken: " + (string)time); | |||
time = llGetAndResetTime(); | |||
llSay(0, "Bubble Sort Result: \n" + llList2CSV(llListSort(elements, 2, TRUE))); | |||
llSay(0, "Time Taken: " + (string)time); | |||
} | |||
} | |||
</source> | |||
Can others proofread and/or test this with other values and confirm that I am not producing false results? | |||
If does turn out to be more efficient, please feel free to use it in your own projects. | |||
::I'm afraid your timing system is faulty. You're not reading the times at the right points in your code. I'm afraid on an admitedly very quick test llListSort() wins by a long chalk. Never-the-less, bravo on getting your merge sort to work. | |||
<source lang="lsl2"> | |||
touch_start(integer num) | |||
{ | |||
llSay(0, "Input: \n Merge Sort:" + llList2CSV(elements)); | |||
llResetTime(); | |||
list lx = ListSortFast(elements, 2); | |||
float time = llGetTime(); | |||
llSay(0, "Time Taken MergeSort: " + (string)time); | |||
llSay(0, "Sorted Result: \n" + llList2CSV(lx)); | |||
llSleep(1); | |||
llResetTime(); | |||
lx = llListSort(elements, 2, TRUE); | |||
time = llGetTime(); | |||
llSay(0, "Time Taken llListSort: " + (string)time); | |||
llSay(0, "Bubble Sort Result: \n" + llList2CSV(lx)); | |||
} | |||
</source> | |||
p.s. On a test of 5000 sorts of your data, the merge sort is 5.5 times slower than llListSort(). Sorry! | |||
[[User:Omei Qunhua|Omei Qunhua]] 15:09, 22 May 2014 (PDT) |
Latest revision as of 04:52, 5 February 2015
Bubble Sort?
This script:
default
{
state_entry()
{
llOwnerSay(llList2CSV(llListSort([3,2,3,5,3,1,1,4], 2, TRUE)));
}
}
outputs
1, 4, 3, 5, 3, 1, 3, 2
both in LSO and in Mono. This means that the sort is not stable. Since stability is a characteristic of Bubble Sort, it looks like the claim of using Bubble Sort is not correct. --Pedro Oval 20:01, 5 April 2014 (PDT)
maybe... needs an extended example to see if LSL backwards ordering isn't the culprit
-- Void (talk|contribs) 22:19, 5 April 2014 (PDT)
- If it's in reverse order, it's not stable either. The reverse would be 3,1,3,5,3,2 but the actual result is 3,5,3,1,3,2 --Pedro Oval 12:39, 6 April 2014 (PDT)
doh, you're right... comb sort perhaps? looking at the ordering exchange seems more likely than insertion
-- Void (talk|contribs) 17:20, 6 April 2014 (PDT)
- Best option could be to pose this question on the scripting mail list, Kelly appears to prefer that channel for tech information dumps. The bubble sort presumption probably came from a function included with the viewer part of the source, but its comments describe list syntax and functions that never made it to LSL2. --ObviousAltIsObvious Resident 17:30, 6 April 2014 (PDT)
- I think it was a semantic problem, the existing list functions don't leave any room for embedded lists. When they couldn't get pass-by-reference to work properly (assumption), they likely killed embedded lists because it didn't fit well in 16KiB.
- Q: Why does the viewer source contain code that is obviously server LSL VM source code?
- A: A bit of history, before the age of Mono, scripts were compiled by the client, and the viewer source included a copy of much of the server VM code. You have to remember that the viewer wasn't always open source, it's likely that the client and server code once existed in a single repository, that would be recompiled at the same time. When LL opensourced the viewer, they split the repository, but because of interdependence between the server and client code, some files were duplicated between the two repositories. LSL related files overtime diverged, largely driven by Mono's server side compilation. So while it may seem strange now that the viewer contains server source, it wasn't at the time the article was written.
- -- Strife (talk|contribs) 19:08, 6 April 2014 (PDT)
It's probably not very polite to bring up this link, but it may shed some light on the reasons behind usage of Bubble Sort and the presumption that it was used in the server code: [1]. It mentions what it calls "type specific feathered sorting", which I assume it to mean the individual sorting of elements of each type. The code claims that "unoptimized bubble sort" is the only way to achieve it. I haven't given it much thought yet, but I wonder if comb sort, as mentioned by Void, would also preserve that ordering. --Pedro Oval 03:09, 8 April 2014 (PDT)
- I think that 3 passes index, sort, write could do this as well. Miranda's comment in SVC-2988 suggests that there is some other kind of overhead in there. --ObviousAltIsObvious Resident 05:31, 8 April 2014 (PDT)
- I've tried Quicksort and Comb Sort. My input list was [3,2,1,3,3,4,3,3,1,4,1,9,3,6] with stride 2. llListSort returns [1,3,1,4,1,9,3,3,3,2,3,4,3,6]. I could not reproduce this result with Comb sort, not even pre-inverting the array nor by pre-inverting, sorting descending and post-inverting it. I've tried two variations and all possible shrink values for this length. I've also tried with Quicksort with a decent amount of possible choices of pivot (first, central, last, median, and sorting the first, central and last elements, using several variations of < and <= for median and sorting), to no avail. --Pedro Oval 07:01, 8 April 2014 (PDT)
- I wish I had more time to play with this. Going by what is typically found in LL code, my first guess would be "whatever the GNU library is using in std::sort". It could very well be a wrapper around that. Adding to fun, there are 2 major C++ libraries in GNU land, each uses a different approach. --ObviousAltIsObvious Resident 07:30, 8 April 2014 (PDT)
- Same difference, as Boost uses std::sort: [2]. Digging a bit I have found this: Introsort#Implementations (Wikipedia) which made it sound like any attempt at reproducing the results without GNU libstdc++ was going to be too painful to be worth it. Then I've tried with the C++11 program below; it was sorted in a stable way, as the result is: "1,3, 1,4, 1,9, 3,2, 3,4, 3,3, 3,6, ". I'm willing to make more experiments but I'm out of ideas at the moment. Any suggestions? (P.S.: who chose the colors for the C++ highlighter? Yuck!) --Pedro Oval 09:30, 9 April 2014 (PDT)
#include <iostream>
#include <algorithm>
#include <vector>
struct Rec
{
int key;
int data;
};
bool sortByKey(const Rec &lhs, const Rec &rhs) { return lhs.key < rhs.key; }
int main()
{
std::vector<Rec> v = {{3,2},{1,3},{3,4},{3,3},{1,4},{1,9},{3,6}};
std::sort(v.begin(), v.end(), sortByKey);
for (Rec &n : v)
std::cout << n.key << "," << n.data << ", ";
std::cout << std::endl;
return 0;
}
- tried substituting partial_sort (I think that bypasses the insertion sort libstdc++ likes to use on small sets), almost the same as LSL but not close enough. 1,4, 1,3, 1,9, 3,3, 3,2, 3,4, 3,6, --ObviousAltIsObvious Resident 15:34, 9 April 2014 (PDT)
I don t know why you are using boost c++ library .
Probably the key of the answer is inside the implementation of sort in the mono library .
I dont know which version mono is , and which algorithm it uses for sort . Nevertheless , the documentation from microsoft on dotnet c# gives :
from [ http://msdn.microsoft.com/en-us/library/b0zbh7b6(v=vs.110).aspx ]
This method uses the Array.Sort method, which applies the introspective sort as follows: * If the partition size is fewer than 16 elements, it uses an insertion sort algorithm. * If the number of partitions exceeds 2 * LogN, where N is the range of the input array, it uses a Heapsort algorithm. * Otherwise, it uses a Quicksort algorithm. This implementation performs an unstable sort; that is, if two elements are equal, their order might not be preserved. In contrast, a stable sort preserves the order of elements that are equal. On average, this method is an O(n log n) operation, where n is Count; in the worst case it is an O(n ^ 2) operation.
We should verify if the actual version of mono used by linden uses the same strategy as microsoft.
But , at first view , it seems coherent with the actual observations
Other note : a bubble sort is O(n) if the list is already sorted . Some other algorithms too.
And some other algorithms , not.
But in LSL/mono , sortig an already sorted list don t make it faster
Miranda Umino 20:55, 17 April 2014 (PDT)
- The sort works the same under LSO, so I believe we can rule out a native .net library call as the source of this behavior. --ObviousAltIsObvious Resident 00:29, 18 April 2014 (PDT)
I'm still researching, but I'll advance some information. I've reproduced the results with this script, which implements a very simple sort algorithm that is not listed in Wikipedia:
list Sort(list input, integer stride)
{
integer i;
integer j;
for (i = 0; i < llGetListLength(input) - stride; i += stride)
{
for (j = i + stride; j < llGetListLength(input); j += stride)
{
if (llList2Integer(input, i) > llList2Integer(input, j))
{
// Swap records i & j
input = llListReplaceList(
llListReplaceList(input, llList2List(input, j, j+stride-1), i, i+stride-1),
llList2List(input, i, i+stride-1), j, j+stride-1);
}
}
}
return input;
}
default
{
state_entry()
{
list MyList = [3,2,1,3,3,4,3,3,1,4,1,9,3,6];
string L1 = llList2CSV(llListSort(MyList, 2, TRUE));
string L2 = llList2CSV(Sort(MyList, 2));
llOwnerSay("O: " + L1);
llOwnerSay("N: " + L2);
if (L1 != L2) llOwnerSay("Different"); else llOwnerSay("Equal");
}
}
// Output:
// [09:50:20] Sort: O: 1, 3, 1, 4, 1, 9, 3, 3, 3, 2, 3, 4, 3, 6
// [09:50:20] Sort: N: 1, 3, 1, 4, 1, 9, 3, 3, 3, 2, 3, 4, 3, 6
// [09:50:20] Sort: Equal
I realized it was a candidate while trying to understand how bubble sort would preserve the ordering as claimed by the OpenSim code. I couldn't, so I looked into the code in more detail, and I realized that the sorting algorithm used was not bubble sort, but the one implemented in the above script. It's a naïve algorithm similar to selection sort, but less optimal: it goes through each element, and does a sweep over the rest of the elements to see if there's one less than it. If so, it swaps them (selection sort just takes note of the index position of the least element, and swaps them at the end of the inner loop instead of as it goes). At the end of each sweep, the element in question will have the least of the remaining elements in the list. It's still O(n²), and worse in performance than selection sort (since it does more swaps), but it does indeed guarantee preservation of the order of the types as claimed, assuming that comparison of two elements of different types does not cause a swap. It's also not stable, by its nature, and as proved by the results above. --Pedro Oval 10:09, 19 April 2014 (PDT)
Update: Selection Sort fails to reproduce the results, which is expected. However, the above algorithm matched the results every time I've tried. I tested with this list generation code in place of the constant:
list MyList;
while (llGetListLength(MyList) < 500)
{
MyList = MyList + (integer)llFrand(12) + llGetListLength(MyList);
}
It seems we have a winner. --Pedro Oval 10:51, 19 April 2014 (PDT)
Update 2: It gets better. Type feathering is NOT respected in case of a descending sort. OpenSim gives different results to SL in that case. It looks like type feathering is not a feature, but a side effect of the algorithm used. I've reproduced the results by always swapping when the types are different. The results I got are consistent with this: the comparison function returns TRUE if A and B are of the same type and A is greater than B, and FALSE otherwise, and the swap is done if (the comparison function returns TRUE) xor (ascending!=1), causing it to always swap elements of different types for descending sort, thus destroying the feathering. OpenSim respects the feathering for descending order. --Pedro Oval 14:38, 19 April 2014 (PDT)
One sure thing : llListSort is not O(N*N) . LLstsort is O(N * Log2(N) )
Here some results of your algorithm ( 2nd column = number of pairs ; 3rd column time in frames per iteration)
BubbleSort NumberTest N size Average Time in Frames Test #1 with 2 0.020000 Test #2 with 3 0.050000 Test #3 with 4 0.090000 Test #4 with 6 0.200000 Test #5 with 8 0.200000 Test #6 with 12 0.650000 Test #7 with 16 0.970000 Test #8 with 24 1.850000 Test #9 with 32 2.300000 Test #10 with 48 5.590000 Test #11 with 64 7.540000 Test #12 with 96 18.270000 Test #13 with 128 28.950000 Test #14 with 160 53.440000 Test #15 with 192 71.920000 Test #16 with 224 90.050000 Test #17 with 256 112.140000
Time ( 2*128 ) = (2*2) * Time(128) => ALgorithm is O(N*N)
And with LListSort
LListSort Test #1 with 2 0.040000 Test #2 with 3 0.070000 Test #3 with 4 0.040000 Test #4 with 6 0.050000 Test #5 with 8 0.070000 Test #6 with 12 0.150000 Test #7 with 16 0.080000 Test #8 with 24 0.210000 Test #9 with 32 0.320000 Test #10 with 48 0.340000 Test #11 with 64 0.430000 Test #12 with 96 0.280000 Test #13 with 128 0.670000 Test #14 with 160 0.650000 Test #15 with 192 0.760000 Test #16 with 224 0.660000 Test #17 with 256 0.840000
Time ( 2*128 ) = 2*log(2) * Time(128) => ALgorithm is O(N*LogN)
list Sort(list input, integer stride)
{
integer i;
integer j;
for (i = 0; i < llGetListLength(input) - stride; i += stride)
{
for (j = i + stride; j < llGetListLength(input); j += stride)
{
if (llList2Integer(input, i) > llList2Integer(input, j))
{
// Swap records i & j
input = llListReplaceList(
llListReplaceList(input, llList2List(input, j, j+stride-1), i, i+stride-1),
llList2List(input, i, i+stride-1), j, j+stride-1);
}
}
}
return input;
}
default {
state_entry() {
list tests = [
// Ascending lists
2, TRUE,
3, TRUE,
4, TRUE,
6, TRUE,
8, TRUE,
12, TRUE,
16, TRUE,
24, TRUE,
32, TRUE,
48, TRUE,
64, TRUE,
96, TRUE,
128, TRUE,
160, TRUE,
192, TRUE,
224, TRUE,
256, TRUE
/* 320, TRUE,
384, TRUE,
448, TRUE,
512, TRUE
*/
];
integer repetitions = 100;
integer test = 0; integer testsLength = tests != [];
integer listSize; integer ascending; integer i; list t;
float f; string s; list l = [];
float nFrameStart;
float nFrameEnd;
float overhead;
llOwnerSay("Starting tests…");
for (; test < testsLength; test += 2) {
listSize = llList2Integer(tests, test);
ascending = llList2Integer(tests, test + 1);
if (ascending) {
i = 0;
for (; i < listSize; ++i)
l += [(integer)llFrand(3),llFrand(10000)] ;
} else {
i = listSize;
while ((--i) >= 0)
l += (integer)llFrand(10000);
}
i = 0;
nFrameStart = (float)llGetEnv("frame_number");
for (; i < repetitions; ++i)
t = [];
nFrameEnd = (float)llGetEnv("frame_number");
overhead = nFrameEnd - nFrameStart;
i = 0;
nFrameStart = (float)llGetEnv("frame_number");
for (; i < repetitions; ++i)
// t = llListSort(l, 2, ascending);
t = Sort(l,2);
nFrameEnd = (float)llGetEnv("frame_number");
f = nFrameEnd - nFrameStart - overhead;
t = l = [];
f /= (float)repetitions;
s = "\tTest #" + (string)((test / 2) + 1) + " with\t" + (string)listSize + "\t" + (string)( f );
llOwnerSay(s);
}
llOwnerSay("Tests complete.");
}
}
--Miranda Umino 21:41, 19 April 2014 (PDT)
- It's definitively quadratic, i.e. O(n²). http://inky.ws/g/2zz --Pedro Oval 19:39, 5 May 2014 (PDT)
- Update: Added benchmarking script to the linked gallery. --Pedro Oval 06:15, 6 May 2014 (PDT)
- Update 2: I tried the same benchmarking script listed in Inky under LSO and the strangest thing happened. I changed the list addition line to use the
(MyList = []) + MyList
trick, which allowed for an extra batch of 50. Here are the results I got:
N Time 50 3.333132 100 6.786429 150 13.334011 200 13.333488 250 13.333256 300 13.333005 350 13.332855 400 13.334793 450 13.334989 500 13.332029
- Looks like under LSO the function is being throttled. --Pedro Oval 13:36, 6 May 2014 (PDT)
- Update 3: It seems it's throttled under Mono as well, just not so much. I've added to the gallery a graphic with a benchmark over the first 200-300 values, going through them one at a time. The delay seems roughly linear on N up to about N=80. Then it gets constant (to almost 3 seconds, actually about 2.95 or 2.97, according to the data, meaning almost 10 ms per call) until the actual time the function takes overtakes the throttle. When does that happen, depends on what is being compared (I used integers and vectors because they have a noticeably different comparison time) and on the performance of the sim (green line was taken on a faster sim).
- The benchmarking script is the same, just setting Step=1 and MaxN=300. The vectors were constant, set to <1,2,3>. For the descending test, I just changed all TRUE to FALSE. The first run is up to N=200 only because I thought it was enough, but when I finished and graphed it, it wasn't clear enough from the results I got, so I raised it to 300.
- I conjecture that the spikes are GC kicking in and may depend on available memory. The sim I went to to take the sample with the green line was much emptier. --Pedro Oval 05:07, 19 May 2014 (PDT)
Merge Sort in LSL
I've written a Merge Sort that's nearly comparable to the llListSort provided by LSL. It supports strided lists (as in the example below), but cannot reverse the order as llListSort does. I ran this test a few times on it, and surprisingly, it's faster than llListSort. The code:
list elements = [8.627350, "a3c6cf64-8daf-b6ca-9851-8a49a597fafc", 18.943806, "3e0a230a-16ef-f539-b070-0ef0a16b1835", 6.843595, "aa3183b2-a5ad-abb3-98c2-c07517c049b5", 5.199630, "513ec659-a280-5ea4-ab3c-5f3a5544820e", 14.510715, "7f1f52bc-c63f-aef0-73c9-cd97f1bddccc", 12.230530, "3c719f4f-072d-b35f-9fe6-ed157e06c487", 3.188807, "a84db094-9630-79fc-eb02-74bcbd030190", 1.648486, "e0a8af06-2c43-57df-9571-22c035424acc", 8.481318, "891c3eba-3723-29c4-f1ea-032b6dad2b1b", 17.444149, "e4b04775-8d68-0d4a-773f-19f02c112066"];
list merge(list left, list right, integer stride)
{
list result;
while(llGetListLength(left) > 0 || llGetListLength(right) > 0)
{
if(llGetListLength(left) > 0 && llGetListLength(right) > 0)
{
// Ensure we're comparing the same things...
if(llGetListEntryType(left, 0) == llGetListEntryType(right, 0))
{
// Get values in a re-cast-able form
integer type = llGetListEntryType(left, 0);
string InitialLeftValue = llList2String(left, 0);
string InitialRightValue = llList2String(right, 0);
// Handle all type cases
if(type == TYPE_INTEGER)
{
integer leftValue = (integer)InitialLeftValue;
integer rightValue = (integer)InitialRightValue;
if(leftValue <= rightValue)
{
result += llList2List(left, 0, 1);
left = llDeleteSubList(left, 0, stride - 1);
}
else
{
result += llList2List(right, 0, 1);
right = llDeleteSubList(right, 0, stride - 1);
}
}
else if(type == TYPE_FLOAT)
{
float leftValue = (float)InitialLeftValue;
float rightValue = (float)InitialRightValue;
if(leftValue <= rightValue)
{
result += llList2List(left, 0, 1);
left = llDeleteSubList(left, 0, stride - 1);
}
else
{
result += llList2List(right, 0, 1);
right = llDeleteSubList(right, 0, stride - 1);
}
}
else if(type == TYPE_VECTOR)
{
float leftValue = llVecMag((vector)InitialLeftValue);
float rightValue = llVecMag((vector)InitialRightValue);
if(leftValue <= rightValue)
{
result += llList2List(left, 0, 1);
left = llDeleteSubList(left, 0, stride - 1);
}
else
{
result += llList2List(right, 0, 1);
right = llDeleteSubList(right, 0, stride - 1);
}
}
else if(type == TYPE_ROTATION)
{
float leftValue = llRot2Angle((rotation)InitialLeftValue);
float rightValue = llRot2Angle((rotation)InitialRightValue);
if(leftValue <= rightValue)
{
result += llList2List(left, 0, 1);
left = llDeleteSubList(left, 0, stride - 1);
}
else
{
result += llList2List(right, 0, 1);
right = llDeleteSubList(right, 0, stride - 1);
}
}
}
}
else if(llGetListLength(left) > 0)
{
result += llList2List(left, 0, 1);
left = llDeleteSubList(left, 0, stride - 1);
}
else if(llGetListLength(right) > 0)
{
result += llList2List(right, 0, 1);
right = llDeleteSubList(right, 0, stride - 1);
}
}
return result;
}
list llListSortFast(list in, integer stride)
{
if(llGetListLength(in) <= stride)
{
return in;
}
integer middle = (llGetListLength(in) / stride) / 2;
list left = llList2List(in, 0, (middle) * stride - 1);
list right = llList2List(in, middle * stride, -1);
left = llListSortFast(left, stride);
right = llListSortFast(right, stride);
return merge(left, right, stride);
}
default
{
state_entry()
{
llSay(0, "Input: \n Merge Sort:" + llList2CSV(elements));
float time = llGetAndResetTime();
llSay(0, "Sorted Result: \n" + llList2CSV(llListSortFast(elements, 2)));
llSay(0, "Time Taken: " + (string)time);
time = llGetAndResetTime();
llSay(0, "Bubble Sort Result: \n" + llList2CSV(llListSort(elements, 2, TRUE)));
llSay(0, "Time Taken: " + (string)time);
}
}
Can others proofread and/or test this with other values and confirm that I am not producing false results?
If does turn out to be more efficient, please feel free to use it in your own projects.
- I'm afraid your timing system is faulty. You're not reading the times at the right points in your code. I'm afraid on an admitedly very quick test llListSort() wins by a long chalk. Never-the-less, bravo on getting your merge sort to work.
touch_start(integer num)
{
llSay(0, "Input: \n Merge Sort:" + llList2CSV(elements));
llResetTime();
list lx = ListSortFast(elements, 2);
float time = llGetTime();
llSay(0, "Time Taken MergeSort: " + (string)time);
llSay(0, "Sorted Result: \n" + llList2CSV(lx));
llSleep(1);
llResetTime();
lx = llListSort(elements, 2, TRUE);
time = llGetTime();
llSay(0, "Time Taken llListSort: " + (string)time);
llSay(0, "Bubble Sort Result: \n" + llList2CSV(lx));
}
p.s. On a test of 5000 sorts of your data, the merge sort is 5.5 times slower than llListSort(). Sorry! Omei Qunhua 15:09, 22 May 2014 (PDT)