I need to process the data contained in a (relatively) large binary file.
The file has the following structure:
40 bytes of initialization,
4 bytes, 1 byte,
4 bytes, 1 byte,
...
lots of such 5-byte blocks
...
The number of 5-byte blocks (to which I'll refer to as 'timetags' in the following) may vary, but the total size of the file can be in the order of ~100 MBs.
In each 5-byte block the first 4 bytes encode a uint32_t
(unsigned integer) 'timestamp', and the fifth byte is a number encoding a 'channel'.
My task is to find out whether there are contiguous sequences of 4 timetags, such that the corresponding timestamps are within a certain time window from each other, and if that is the case store the corresponding channels.
For example, if there is a sequence of timetags whose decoded data is
100, 2
300, 4
310, 5
340, 8,
369, 6,
413, 8
and my time window is 100
, then I store the list [4,5,8,6]
.
In general the number of such fourfold coincidences will be extremely small with respect to the total number of timetags (e.g. for a ~100MB file I have ~10 such coincidences). Also, the timestamps are generally in increasing order, but sometimes there is a sudden jump (when the timestamps becomes too big for the 4 bytes to encode) and the count starts over, and this has to be taken into account (see below for an example file).
I tried a variety of things to do this, but the best I achieved is a ~20s time to process a ~50MB file. However, implementing the same code in C, I get a performance of ~0.2s for a ~100MB file. While I understand that a C program is naturally faster than a python one, such a big performance gap really seems strange to me, so there must be something wrong with my python code.
Here is an example of the binary file to process (this is a reduced version of ~1MB). It contains a single fourfold coincidence at the last four timetags.
Here is my best implementation:
#!/usr/bin/python3
import struct
import os
# ============ CONSTANTS TO (OPTIONALLY) SET =============
WINDOW_SIZE = 100
INPUT_FILE_NAME = 'datafile.bin'
with open(INPUT_FILE_NAME, 'rb') as input_file:
# number of bytes found in every file produced when writing the binary file
initial_offset = 40
# number of bytes occupied by each timetag
timetag_size = 5
# each timetag is stored in 5 bytes. The first 4 bytes contain the
# timestamp, with the first byte being the least significant one, and last
# byte the most significant one
# bytes_to_timestamp expects in input a list of 4 bytes encoding a
# timestamp. The first byte is the least significant one.
def bytes_to_timestamp(bytes):
return(struct.unpack("L", bytes)[0])
# timestamps_in_window expects two 4-byte inputs, and returns whether the
# second one should be considered in coincidence with the first one
def timestamps_in_window(triggering_timestamp_bytes, new_timestamp_bytes):
return(
0 <
bytes_to_timestamp(new_timestamp_bytes) -
bytes_to_timestamp(triggering_timestamp_bytes)
< WINDOW_SIZE
)
file_size = os.path.getsize(INPUT_FILE_NAME)
effective_file_size = file_size - initial_offset - file_size % timetag_size
input_file.seek(initial_offset)
data = input_file.read(effective_file_size)
output_channels = []
triggering_timestamp = data[0:4]
channels = [data[4]]
for timetag_idx in range(effective_file_size // timetag_size - 1):
current_byte = (timetag_idx + 1)*timetag_size
current_timestamp = data[current_byte:current_byte+4]
current_channel = data[current_byte + 4]
if timestamps_in_window(triggering_timestamp, current_timestamp):
channels.append(current_channel)
else:
if len(channels) == 4:
print('found a fourfold coincidence at ', end = '')
for channel in channels:
print('{0: >5}'.format(channel), end = '')
print('')
output_channels.append(channels)
channels = [current_channel]
triggering_timestamp = current_timestamp
if len(channels) == 4:
print('found a fourfold coincidence at ', end = '')
for channel in channels:
print('{0: >5}'.format(channel), end = '')
print('')
output_channels.append(channels)
I also tried to implement this with numpy.fromfile
, but I ended up with a much slower code, which you can check out in this GitHub Gist.
How can I make the code more efficient?
100, 150, 170, 220, 230, 500, 600
, the150, 170, 220, 230
sequence will not be detected (because the220
will be thetriggering_timestamp
). It might no matter for your actual problem, but it's a corner case that may influence the code. \$\endgroup\$if len(channels) >= 4:
will efficiently correct this. \$\endgroup\$