ArslanAnjum opened a new pull request, #4181:
URL: https://github.com/apache/iceberg-python/pull/4181

   # perf: optimize deletion vector deserialization using zero-copy memoryview
   
   ## Summary
   Optimizes deletion vector parsing by replacing repeated byte string slicing 
(`pl = pl[4:]`) with zero-copy pointer tracking via Python's built-in 
`memoryview`. 
   
   Repeatedly slicing `bytes` objects creates new allocations and copies 
remaining payload data into memory on every iteration, leading to $O(N^2)$ 
memory copying behavior as payload sizes grow. Utilizing `memoryview` keeps 
memory allocations at $O(1)$ during parsing, improving execution times by up to 
**~39x** on large dense payloads.
   
   ---
   
   ## Benchmark Results
   
   Benchmarks were executed across four real-world Iceberg deletion vector 
scenarios using `pytest-benchmark` (`min_rounds=1000`, `warmup=True`, 
`disable_gc=True`):
   
   | Scenario | Payload Description | Original Mean | PR Mean | Speedup |
   | :--- | :--- | :--- | :--- | :--- |
   | `small_sparse` | 5 bitmaps (~1.5 KB) | **7.07 µs** | **7.49 µs** | 
**0.94x** *(~5% overhead on tiny buffers)* |
   | `gaps_sparse_keys` | 100 non-contiguous keys (~130 KB) | **429.36 µs** | 
**238.16 µs** | **1.80x faster** |
   | `medium_typical` | 50 bitmaps (~2.1 MB) | **488.03 µs** | **109.09 µs** | 
**4.47x faster** |
   | `large_dense` | 200 bitmaps (~21 MB) | **25,342.14 µs** | **645.55 µs** | 
**39.26x faster** |
   
   ### Detailed Terminal Benchmark Output
   
   ```text
   ----------------------------------------------------------------------- 
benchmark 'scenario_name=gaps_sparse_keys': 2 tests 
------------------------------------------------------------------------
   Name (time in us)                                  Mean              StdDev  
            Median            OPS (Kops/s)            Rounds       Min          
         Max                IQR
   
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   test_benchmark_pr[gaps_sparse_keys]           238.1630 (1.0)      44.6340 
(1.0)      230.0540 (1.0)            4.1988 (1.0)        10869  180.6580 (1.0)  
    1,048.9360 (1.0)      20.9197 (1.0)
   test_benchmark_original[gaps_sparse_keys]     429.3581 (1.80)     68.4834 
(1.53)     426.8960 (1.86)           2.3291 (0.55)        5777  342.4320 (1.90) 
    1,397.9330 (1.33)     32.2145 (1.54)
   
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   
   
------------------------------------------------------------------------------- 
benchmark 'scenario_name=large_dense': 2 tests 
------------------------------------------------------------------------------
   Name (time in us)                                Mean                 StdDev 
                Median                   OPS            Rounds          Min     
               Max                    IQR
   
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   test_benchmark_pr[large_dense]               645.5468 (1.0)        114.8522 
(1.0)         633.8070 (1.0)       1,549.0743 (1.0)         3769     521.7980 
(1.0)       2,065.5120 (1.0)          39.9387 (1.0)
   test_benchmark_original[large_dense]     25,342.1420 (39.26)    1,427.5573 
(12.43)    25,053.5565 (39.53)        39.4600 (0.03)        1000  23,261.5680 
(44.58)    34,868.0240 (16.88)    1,048.5725 (26.25)
   
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   
   ----------------------------------------------------------------------- 
benchmark 'scenario_name=medium_typical': 2 tests 
------------------------------------------------------------------------
   Name (time in us)                                Mean              StdDev    
          Median            OPS (Kops/s)            Rounds       Min            
       Max                IQR
   
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   test_benchmark_pr[medium_typical]           109.0941 (1.0)      21.4579 
(1.0)      107.1850 (1.0)            9.1664 (1.0)        23456   86.7770 (1.0)  
      833.7140 (1.0)        5.3600 (1.0)
   test_benchmark_original[medium_typical]     488.0276 (4.47)     62.2185 
(2.90)     481.1955 (4.49)           2.0491 (0.22)        4674  437.6670 (5.04) 
    1,633.0450 (1.96)     20.8380 (3.89)
   
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   
   ------------------------------------------------------------------ benchmark 
'scenario_name=small_sparse': 2 tests 
-------------------------------------------------------------------
   Name (time in us)                            Mean            StdDev          
  Median            OPS (Kops/s)            Rounds     Min                   
Max                IQR
   
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   test_benchmark_original[small_sparse]       7.0736 (1.0)      2.7843 (1.16)  
    6.9530 (1.0)          141.3713 (1.0)       348432  5.7280 (1.0)       
680.0050 (1.01)     0.4060 (1.0)
   test_benchmark_pr[small_sparse]             7.4874 (1.06)     2.4043 (1.0)   
    7.4810 (1.08)         133.5569 (0.94)      324571  5.8790 (1.03)      
672.9530 (1.0)      0.5870 (1.45)
   
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
   ```
   
   ## Reproducing Benchmarks
   
   Run the benchmark script below using `pytest-benchmark`:
   
   ```bash
   uv run --with pytest-benchmark pytest test_deletion_vector_benchmark.py \
     --benchmark-columns=mean,stddev,median,ops,rounds,min,max,iqr \
     --benchmark-sort=mean \
     --benchmark-group-by=param:scenario_name \
     -q
     
   ```
     
   ```code
   import io
   import pytest
   from pyroaring import BitMap, FrozenBitMap
   
   EMPTY_BITMAP = FrozenBitMap()
   MAX_JAVA_SIGNED = 2147483647
   
   
   def deserialize_original(pl: bytes) -> list[BitMap]:
       number_of_bitmaps = int.from_bytes(pl[0:8], byteorder="little")
       pl = pl[8:]
   
       bitmaps = []
       last_key = -1
       for _ in range(number_of_bitmaps):
           key = int.from_bytes(pl[0:4], byteorder="little")
           if key < 0:
               raise ValueError(f"Invalid unsigned key: {key}")
           if key <= last_key:
               raise ValueError("Keys must be sorted in ascending order")
           if key > MAX_JAVA_SIGNED:
               raise ValueError(
                   f"Key {key} is too large, max {MAX_JAVA_SIGNED} to maintain 
compatibility with Java impl"
               )
           pl = pl[4:]
   
           while last_key < key - 1:
               bitmaps.append(EMPTY_BITMAP)
               last_key += 1
   
           bm = BitMap.deserialize(pl)
           pl = pl[len(bm.serialize()) :]
           bitmaps.append(bm)
   
           last_key = key
   
       return bitmaps
   
   
   def deserialize_pr(pl: bytes) -> list[BitMap]:
       number_of_bitmaps = int.from_bytes(pl[0:8], byteorder="little")
       offset = 8
   
       bitmaps = []
       last_key = -1
       buffer = memoryview(pl)
   
       for _ in range(number_of_bitmaps):
           key = int.from_bytes(buffer[offset : offset + 4], byteorder="little")
           if key < 0:
               raise ValueError(f"Invalid unsigned key: {key}")
           if key <= last_key:
               raise ValueError("Keys must be sorted in ascending order")
           if key > MAX_JAVA_SIGNED:
               raise ValueError(
                   f"Key {key} is too large, max {MAX_JAVA_SIGNED} to maintain 
compatibility with Java impl"
               )
           offset += 4
   
           while last_key < key - 1:
               bitmaps.append(EMPTY_BITMAP)
               last_key += 1
   
           bm = BitMap.deserialize(buffer[offset:])
           offset += len(bm.serialize())
           bitmaps.append(bm)
   
           last_key = key
   
       return bitmaps
   
   
   def create_sample_payload(num_bitmaps=50, values_per_bitmap=20_000) -> bytes:
       buf = io.BytesIO()
       buf.write(num_bitmaps.to_bytes(8, byteorder="little"))
   
       for key in range(num_bitmaps):
           buf.write(key.to_bytes(4, byteorder="little"))
           bm = BitMap(range(0, values_per_bitmap * 2, 2))
           buf.write(bm.serialize())
   
       return buf.getvalue()
   
   
   def create_sample_payload_with_gaps(num_bitmaps=50, step=5, 
values_per_bitmap=1_000) -> bytes:
       buf = io.BytesIO()
       buf.write(num_bitmaps.to_bytes(8, byteorder="little"))
   
       current_key = 0
       for _ in range(num_bitmaps):
           buf.write(current_key.to_bytes(4, byteorder="little"))
           bm = BitMap(range(0, values_per_bitmap * 2, 2))
           buf.write(bm.serialize())
           current_key += step
   
       return buf.getvalue()
   
   
   BENCHMARK_SCENARIOS = {
       "small_sparse": create_sample_payload(num_bitmaps=5, 
values_per_bitmap=100),
       "medium_typical": create_sample_payload(num_bitmaps=50, 
values_per_bitmap=20_000),
       "large_dense": create_sample_payload(num_bitmaps=200, 
values_per_bitmap=50_000),
       "gaps_sparse_keys": create_sample_payload_with_gaps(num_bitmaps=100, 
step=10, values_per_bitmap=500),
   }
   
   
   @pytest.mark.benchmark(
       min_rounds=1000,
       max_time=2.0,
       warmup=True,
       disable_gc=True
   )
   @pytest.mark.parametrize(
       "scenario_name, payload_bytes", 
       BENCHMARK_SCENARIOS.items(), 
       ids=BENCHMARK_SCENARIOS.keys()
   )
   def test_benchmark_original(benchmark, scenario_name, payload_bytes):
       benchmark(deserialize_original, payload_bytes)
   
   
   @pytest.mark.benchmark(
       min_rounds=1000,
       max_time=2.0,
       warmup=True,
       disable_gc=True
   )
   @pytest.mark.parametrize(
       "scenario_name, payload_bytes", 
       BENCHMARK_SCENARIOS.items(), 
       ids=BENCHMARK_SCENARIOS.keys()
   )
   def test_benchmark_pr(benchmark, scenario_name, payload_bytes):
       benchmark(deserialize_pr, payload_bytes)
       
   ```  


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to