Collections¶
Five problems written with ppy.Vec, ppy.Deque, ppy.Heap,
ppy.LinkedList, ppy.HashMap, and ppy.TreeSet, with no pointers in sight.
Every function goes native, and the program prints the same thing on all
three paths.
Run it¶
What it prints¶
python graphs.ppy, ppy run graphs.ppy, ppy build --standalone graphs.ppy -o dist && ./dist/graphs
The problems¶
shortest_paths: Dijkstra over 200,000 nodes with four weighted edges each. The frontier is aHeap[int]holdingdistance * NODES + node, so one integer carries both.hops: breadth-first search over the same graph with aDeque[int], reporting the farthest node in edges.josephus: 100,000 people in a circle, every seventh removed. The circle is aLinkedList[int], walked by node id withnextand cut withremove.repeats: counts 2,000,000 keys in aHashMap[int, int]withget(key, 0) + 1, then walks the keys in insertion order.closest_gaps: the smallest gap between 200,000 values as they arrive, using aTreeSet[int]'sfloorandceilingfor each value's neighbors.
dist and seen are Vec[int](NODES), which start with NODES zeros, as
[0] * NODES would.
How it runs natively¶
ppy explain graphs.shortest_paths and the others each say
llvm backend: native. A collection that a function makes is a handle into
the C runtime in ppy_runtime/collections.py, freed when the function
returns. A check CPython would raise for, such as an empty pop or a
missing key, is a guard: under ppy run it hands the call back to Python,
which raises, and a standalone binary stops.
Timing¶
One machine, five runs each, wall time for the whole program:
| seconds | |
|---|---|
python graphs.ppy (the reference classes under CPython) |
4.3 |
idiomatic.py: the same algorithms with list, deque, heapq, dict, bisect |
2.3 |
ppy run graphs.ppy, after the first run built the cache |
0.39 |
./dist/graphs, the standalone binary |
0.19 |
The reference classes are slower than Python's built-ins because every
method is a Python call. idiomatic.py is there so the comparison is with
code a Python programmer would write. Its josephus pops from a list rather
than walking a linked list, which is the usual way to write it in Python.
Where the code comes from¶
graphs.ppy and idiomatic.py are hand-written.
Read on: the collections guide.
47_collections/graphs.ppy¶
"""Graph and sequence problems written with ppy's collections."""
from ppy import Deque, HashMap, Heap, LinkedList, TreeSet, Vec
NODES = 200000
EDGES = 4
def target(node: int, k: int) -> int:
return (node * 7919 + k * 104729 + 13) % NODES
def weight(node: int, k: int) -> int:
return (node * 31 + k * 17) % 97 + 1
def shortest_paths(source: int) -> int:
dist = Vec[int](NODES)
for i in range(NODES):
dist[i] = -1
frontier = Heap[int]()
frontier.push(source)
dist[source] = 0
while frontier:
packed: int = frontier.pop()
node: int = packed % NODES
here: int = packed // NODES
if here > dist[node]:
continue
for k in range(EDGES):
other: int = target(node, k)
through: int = here + weight(node, k)
if dist[other] < 0 or through < dist[other]:
dist[other] = through
frontier.push(through * NODES + other)
total: int = 0
for d in dist:
total += d
return total
def hops(source: int) -> int:
seen = Vec[int](NODES)
for i in range(NODES):
seen[i] = -1
queue = Deque[int]()
queue.push_back(source)
seen[source] = 0
farthest: int = 0
while queue:
node: int = queue.pop_front()
for k in range(EDGES):
other: int = target(node, k)
if seen[other] < 0:
seen[other] = seen[node] + 1
farthest = max(farthest, seen[other])
queue.push_back(other)
return farthest
def josephus(people: int, step: int) -> int:
circle = LinkedList[int]()
for i in range(people):
circle.push_back(i)
node: int = circle.head()
while len(circle) > 1:
for _ in range(step - 1):
node = circle.next(node)
if node == -1:
node = circle.head()
following: int = circle.next(node)
circle.remove(node)
node = following if following != -1 else circle.head()
return circle.front()
def repeats(count: int) -> int:
seen = HashMap[int, int]()
for i in range(count):
key: int = (i * i + 7 * i) % 65537
seen[key] = seen.get(key, 0) + 1
most: int = 0
for key in seen:
most = max(most, seen[key])
return len(seen) * 100 + most
def closest_gaps(count: int) -> int:
marks = TreeSet[int]()
best: int = 1 << 40
for i in range(count):
value: int = (i * 2654435761) % 1000003
if marks:
if value >= marks.min():
best = min(best, value - marks.floor(value))
if value <= marks.max():
best = min(best, marks.ceiling(value) - value)
marks.add(value)
return best
def main() -> None:
print(shortest_paths(0), hops(0))
print(josephus(100000, 7), repeats(2000000), closest_gaps(200000))
main()
Source: examples/47_collections.