When analyzing different method for getting the element from a single element set, I find that a for loop is faster than .pop(), which surprised me a lot.
# ======================================================================
# RANKED BY MEDIAN TIME (fastest first):
# ======================================================================
# 1. for loop 0.05512s (18.37ns/op, 1.00x)
# 2. value, = s 0.06293s (20.98ns/op, 1.14x)
# 3. next(iter(s)) 0.07988s (26.63ns/op, 1.45x)
# 4. s.pop() 0.08603s (28.68ns/op, 1.56x)
# 5. list(s)[0] 0.13469s (44.90ns/op, 2.44x)
This is my method for establishing the evidence:
import statistics, timeit
setup = "s = {42}"
def bench(stmt, setup=setup, number=3_000_000, repeat=7):
times = timeit.repeat(stmt, setup=setup, number=number, repeat=repeat)
return {
"median": statistics.median(times),
"stdev": statistics.pstdev(times),
"runs": times,
}
results = {
"next(iter(s))": bench("next(iter(s))"),
"s.pop()": bench("s = {42}; s.pop()", setup=""), # Recreate set each time
"value, = s": bench("value, = s"),
"list(s)[0]": bench("list(s)[0]"),
"for loop": bench("for x in s: break"),
}
for k, v in results.items():
print(f"{k:20} {v}")
# Calculate and show rankings
print("\n" + "="*70)
print("RANKED BY MEDIAN TIME (fastest first):")
print("="*70)
sorted_results = sorted(results.items(), key=lambda x: x[1]['median'])
fastest = sorted_results[0][1]['median']
for i, (name, stats) in enumerate(sorted_results, 1):
relative = stats['median'] / fastest
ns_per_op = (stats['median'] / 3_000_000) * 1_000_000_000
print(f"{i}. {name:20} {stats['median']:.5f}s ({ns_per_op:.2f}ns/op, {relative:.2f}x)")
# next(iter(s)) {'median': 0.07987911600048392, 'stdev': 0.0015487941622256854, 'runs': [0.08058188100039843, 0.07924388599985832, 0.08089582999946288, 0.08170180900015112, 0.07987911600048392, 0.07793333700010407, 0.07700227099940093]}
# s.pop() {'median': 0.0860316629996305, 'stdev': 0.0003872496000016712, 'runs': [0.08623365399944305, 0.08636853200005135, 0.085637518999647, 0.0860316629996305, 0.08564598999964801, 0.08562781299951894, 0.0866860940004699]}
# value, = s {'median': 0.06292977600060112, 'stdev': 0.0001934032472859946, 'runs': [0.06281804099944566, 0.06321052299972507, 0.062730442999964, 0.06291875400074787, 0.06293551399994612, 0.0633145620004143, 0.06292977600060112]}
# list(s)[0] {'median': 0.1346858329998213, 'stdev': 0.0014844358476449996, 'runs': [0.13523242300016136, 0.1346858329998213, 0.1344134809996831, 0.13504255899988493, 0.13422213600006216, 0.13427748399953998, 0.13876609099952475]}
# for loop {'median': 0.05512175100011518, 'stdev': 0.00020771769034806938, 'runs': [0.05493225000009261, 0.05492972699994425, 0.05530007499964995, 0.05525388000023668, 0.05512175100011518, 0.0555668480001259, 0.05510627800049406]}
When trying to understand why using dis I take the following approach:
import dis
print("="*70)
print("BYTECODE ANALYSIS OF SET ELEMENT EXTRACTION METHODS")
print("="*70)
# Method 1: for loop
print("\n1. FOR LOOP: for x in s: break")
print("-"*70)
def for_loop():
s = {42}
for x in s:
return x
dis.dis(for_loop)
# Method 2: unpacking
print("\n2. UNPACKING: value, = s")
print("-"*70)
def unpacking():
s = {42}
value, = s
return value
dis.dis(unpacking)
# Method 3: next(iter(s))
print("\n3. NEXT(ITER(S)): next(iter(s))")
print("-"*70)
def next_iter():
s = {42}
return next(iter(s))
dis.dis(next_iter)
# Method 4: s.pop()
print("\n4. S.POP(): s.pop()")
print("-"*70)
def pop_method():
s = {42}
return s.pop()
dis.dis(pop_method)
# Method 5: list(s)[0]
print("\n5. LIST(S)[0]: list(s)[0]")
print("-"*70)
def list_index():
s = {42}
return list(s)[0]
dis.dis(list_index)
print("\n" + "="*70)
print("INSTRUCTION COUNT SUMMARY")
print("="*70)
methods = [
("for loop", for_loop),
("unpacking", unpacking),
("next(iter(s))", next_iter),
("s.pop()", pop_method),
("list(s)[0]", list_index),
]
for name, func in methods:
# Count instructions (excluding the final RETURN_VALUE and setup)
instructions = list(dis.get_instructions(func))
# Filter out RESUME and RETURN_CONST/RETURN_VALUE
core_instructions = [i for i in instructions if i.opname not in ('RESUME', 'RETURN_VALUE', 'RETURN_CONST')]
print(f"{name:20} {len(core_instructions):2} instructions")
and get the following result:
# 1. FOR LOOP: for x in s: break
# ----------------------------------------------------------------------
# 65 0 RESUME 0
# 66 2 LOAD_CONST 1 (42)
# 4 BUILD_SET 1
# 6 STORE_FAST 0 (s)
# 67 8 LOAD_FAST 0 (s)
# 10 GET_ITER
# 12 FOR_ITER 4 (to 24)
# 16 STORE_FAST 1 (x)
# 68 18 POP_TOP
# 69 20 LOAD_FAST 1 (x)
# 22 RETURN_VALUE
# 67 >> 24 END_FOR
# 69 26 LOAD_FAST_CHECK 1 (x)
# 28 RETURN_VALUE
# 2. UNPACKING: value, = s
# ----------------------------------------------------------------------
# 75 0 RESUME 0
# 76 2 LOAD_CONST 1 (42)
# 4 BUILD_SET 1
# 6 STORE_FAST 0 (s)
# 77 8 LOAD_FAST 0 (s)
# 10 UNPACK_SEQUENCE 1
# 14 STORE_FAST 1 (value)
# 78 16 LOAD_FAST 1 (value)
# 18 RETURN_VALUE
# 3. NEXT(ITER(S)): next(iter(s))
# ----------------------------------------------------------------------
# 84 0 RESUME 0
# 85 2 LOAD_CONST 1 (42)
# 4 BUILD_SET 1
# 6 STORE_FAST 0 (s)
# 86 8 LOAD_GLOBAL 1 (NULL + next)
# 18 LOAD_GLOBAL 3 (NULL + iter)
# 28 LOAD_FAST 0 (s)
# 30 CALL 1
# 38 CALL 1
# 46 RETURN_VALUE
# 4. S.POP(): s.pop()
# ----------------------------------------------------------------------
# 92 0 RESUME 0
# 93 2 LOAD_CONST 1 (42)
# 4 BUILD_SET 1
# 6 STORE_FAST 0 (s)
# 94 8 LOAD_FAST 0 (s)
# 10 LOAD_ATTR 1 (NULL|self + pop)
# 30 CALL 0
# 38 RETURN_VALUE
# 5. LIST(S)[0]: list(s)[0]
# ----------------------------------------------------------------------
# 100 0 RESUME 0
# 101 2 LOAD_CONST 1 (42)
# 4 BUILD_SET 1
# 6 STORE_FAST 0 (s)
# 102 8 LOAD_GLOBAL 1 (NULL + list)
# 18 LOAD_FAST 0 (s)
# 20 CALL 1
# 28 LOAD_CONST 2 (0)
# 30 BINARY_SUBSCR
# 34 RETURN_VALUE
# ======================================================================
# INSTRUCTION COUNT SUMMARY
# ======================================================================
# for loop 11 instructions
# unpacking 7 instructions
# next(iter(s)) 8 instructions
# s.pop() 6 instructions
# list(s)[0] 8 instructions
My interpretation of dis is:
- For loop is fastest because:
GET_ITERandFOR_ITERare direct bytecode instructions (no function calls)- Implemented in highly optimized C code
- No global lookups needed
- Unpacking is close second because:
UNPACK_SEQUENCEis a single specialized bytecode instruction- No global lookups or function calls
- Direct operation at the bytecode level
next(iter(s))is slower because:
LOAD_GLOBALtwice (expensive dictionary lookups in builtins namespace)- Two
CALLoperations (function call overhead) - The actual work is fast, but the setup is expensive
list(s)[0]is slowest because:
- Creates an entire list object
- Global lookup + function call + indexing operation
Is this interpretation correct?