1"""Sort performance test.
2
3See main() for command line syntax.
4See tabulate() for output format.
5
6"""
7
8import sys
9import time
10import random
11import marshal
12import tempfile
13import os
14
15td = tempfile.gettempdir()
16
17def randfloats(n):
18    """Return a list of n random floats in [0, 1)."""
19    # Generating floats is expensive, so this writes them out to a file in
20    # a temp directory.  If the file already exists, it just reads them
21    # back in and shuffles them a bit.
22    fn = os.path.join(td, "rr%06d" % n)
23    try:
24        fp = open(fn, "rb")
25    except OSError:
26        r = random.random
27        result = [r() for i in range(n)]
28        try:
29            try:
30                fp = open(fn, "wb")
31                marshal.dump(result, fp)
32                fp.close()
33                fp = None
34            finally:
35                if fp:
36                    try:
37                        os.unlink(fn)
38                    except OSError:
39                        pass
40        except OSError as msg:
41            print("can't write", fn, ":", msg)
42    else:
43        result = marshal.load(fp)
44        fp.close()
45        # Shuffle it a bit...
46        for i in range(10):
47            i = random.randrange(n)
48            temp = result[:i]
49            del result[:i]
50            temp.reverse()
51            result.extend(temp)
52            del temp
53    assert len(result) == n
54    return result
55
56def flush():
57    sys.stdout.flush()
58
59def doit(L):
60    t0 = time.perf_counter()
61    L.sort()
62    t1 = time.perf_counter()
63    print("%6.2f" % (t1-t0), end=' ')
64    flush()
65
66def tabulate(r):
67    r"""Tabulate sort speed for lists of various sizes.
68
69    The sizes are 2**i for i in r (the argument, a list).
70
71    The output displays i, 2**i, and the time to sort arrays of 2**i
72    floating point numbers with the following properties:
73
74    *sort: random data
75    \sort: descending data
76    /sort: ascending data
77    3sort: ascending, then 3 random exchanges
78    +sort: ascending, then 10 random at the end
79    %sort: ascending, then randomly replace 1% of the elements w/ random values
80    ~sort: many duplicates
81    =sort: all equal
82    !sort: worst case scenario
83
84    """
85    cases = tuple([ch + "sort" for ch in r"*\/3+%~=!"])
86    fmt = ("%2s %7s" + " %6s"*len(cases))
87    print(fmt % (("i", "2**i") + cases))
88    for i in r:
89        n = 1 << i
90        L = randfloats(n)
91        print("%2d %7d" % (i, n), end=' ')
92        flush()
93        doit(L) # *sort
94        L.reverse()
95        doit(L) # \sort
96        doit(L) # /sort
97
98        # Do 3 random exchanges.
99        for dummy in range(3):
100            i1 = random.randrange(n)
101            i2 = random.randrange(n)
102            L[i1], L[i2] = L[i2], L[i1]
103        doit(L) # 3sort
104
105        # Replace the last 10 with random floats.
106        if n >= 10:
107            L[-10:] = [random.random() for dummy in range(10)]
108        doit(L) # +sort
109
110        # Replace 1% of the elements at random.
111        for dummy in range(n // 100):
112            L[random.randrange(n)] = random.random()
113        doit(L) # %sort
114
115        # Arrange for lots of duplicates.
116        if n > 4:
117            del L[4:]
118            L = L * (n // 4)
119            # Force the elements to be distinct objects, else timings can be
120            # artificially low.
121            L = list(map(lambda x: --x, L))
122        doit(L) # ~sort
123        del L
124
125        # All equal.  Again, force the elements to be distinct objects.
126        L = list(map(abs, [-0.5] * n))
127        doit(L) # =sort
128        del L
129
130        # This one looks like [3, 2, 1, 0, 0, 1, 2, 3].  It was a bad case
131        # for an older implementation of quicksort, which used the median
132        # of the first, last and middle elements as the pivot.
133        half = n // 2
134        L = list(range(half - 1, -1, -1))
135        L.extend(range(half))
136        # Force to float, so that the timings are comparable.  This is
137        # significantly faster if we leave them as ints.
138        L = list(map(float, L))
139        doit(L) # !sort
140        print()
141
142def main():
143    """Main program when invoked as a script.
144
145    One argument: tabulate a single row.
146    Two arguments: tabulate a range (inclusive).
147    Extra arguments are used to seed the random generator.
148
149    """
150    # default range (inclusive)
151    k1 = 15
152    k2 = 20
153    if sys.argv[1:]:
154        # one argument: single point
155        k1 = k2 = int(sys.argv[1])
156        if sys.argv[2:]:
157            # two arguments: specify range
158            k2 = int(sys.argv[2])
159            if sys.argv[3:]:
160                # derive random seed from remaining arguments
161                x = 1
162                for a in sys.argv[3:]:
163                    x = 69069 * x + hash(a)
164                random.seed(x)
165    r = range(k1, k2+1)                 # include the end point
166    tabulate(r)
167
168if __name__ == '__main__':
169    main()
170