在 Python 中通过循环修改字节串的最快方法?
Fastest method to modify bytestring over loop in Python?
我将图像存储为字节串 b''
并且正在执行逐像素操作。现在我发现最快的方法是使用 struct
crate 在修改期间打包和解包字节,然后将像素保存到 bytearray
# retrieve image data. Stored as bytestring
pixels = buff.get(rect, 1.0, "CIE LCH(ab) alpha double",
Gegl.AbyssPolicy.CLAMP)
# iterator split into 32-byte chunks for each pixel's 8-byte LCHA channels
pixels_iter = (pixels[x:x + 32] for x in range(0, len(pixels), 32))
new_pixels = bytearray()
# when using `pool.map`, the loop was placed in its own function.
for pixel in pixels_iter:
l, c, h, a = struct.unpack('dddd', pixel)
# simple operation for now: lower chroma if bright and saturated
c = c - (l * c) / 100
new_pixels += struct.pack('dddd', l, c, h, a)
# save new data. everything hereout handled by GEGL instead of myself.
shadow.set(rect, "CIE LCH(ab) alpha double", bytes(new_pixels))
问题是我工作站上的 7MP 图像大约需要 3 1/2 秒。如果经常请求更新,则公平但不理想。从我收集到的信息来看,常量数组修改和可能的 struct [un]packing 似乎是罪魁祸首。我已经重构了十几次了,我想我没有优化它的想法。
我试过:
struct.unpack
对整个字节字符串进行一次处理,而不是根据需要对每个像素进行处理。损失了大约 20% 的效率。
collections.deque
诚然不熟悉其技术细节。损失 10-30%,具体取决于实施情况
- 与其他迭代器助手类似的结果,如
map
/join
numpy.array
也诚然对一般的numpy基本一无所知。与 deque 相似的结果
当我将 pool.map
结果附加到 new_pixels
时,multiprocessing
似乎遇到了瓶颈。实际上损失了大约 10%,这看起来很疯狂,因为通常我可以懒洋洋地抛出问题。 pixels_iter
再次 分组为每个线程的相同大小的子列表,因此 new_pixels
连接了 8 个大列表而不是几百万个小列表,我认为这是快点。试图重试这个,因为我可能在凌晨 4 点的实施中以某种方式搞砸了它。
- 理论上,它也可以通过保存图像缓冲区的多个小部分来避免完全连接到
new_pixels
,但这会大大增加其他地方的代码复杂性。
- 将
pixels
本身转换为 bytearray
并使用切片范围就地修改它。损失了 ~30%,但内存使用量也减少了一半。
像 Pypy 这样完全独立的解释器不在 table,因为我不是捆绑 Python 版本的人。
如果使用得当,NumPy 产生的结果应该比手动循环快得多。正确使用它意味着在整个数组上使用 NumPy 操作,而不仅仅是在 NumPy 数组上手动循环。
例如,
new_pixels = bytearray(pixels)
as_numpy = numpy.frombuffer(new_pixels, dtype=float)
as_numpy[1::4] *= 1 - as_numpy[::4] / 100
现在 new_pixels
包含调整后的值。
我将图像存储为字节串 b''
并且正在执行逐像素操作。现在我发现最快的方法是使用 struct
crate 在修改期间打包和解包字节,然后将像素保存到 bytearray
# retrieve image data. Stored as bytestring
pixels = buff.get(rect, 1.0, "CIE LCH(ab) alpha double",
Gegl.AbyssPolicy.CLAMP)
# iterator split into 32-byte chunks for each pixel's 8-byte LCHA channels
pixels_iter = (pixels[x:x + 32] for x in range(0, len(pixels), 32))
new_pixels = bytearray()
# when using `pool.map`, the loop was placed in its own function.
for pixel in pixels_iter:
l, c, h, a = struct.unpack('dddd', pixel)
# simple operation for now: lower chroma if bright and saturated
c = c - (l * c) / 100
new_pixels += struct.pack('dddd', l, c, h, a)
# save new data. everything hereout handled by GEGL instead of myself.
shadow.set(rect, "CIE LCH(ab) alpha double", bytes(new_pixels))
问题是我工作站上的 7MP 图像大约需要 3 1/2 秒。如果经常请求更新,则公平但不理想。从我收集到的信息来看,常量数组修改和可能的 struct [un]packing 似乎是罪魁祸首。我已经重构了十几次了,我想我没有优化它的想法。
我试过:
struct.unpack
对整个字节字符串进行一次处理,而不是根据需要对每个像素进行处理。损失了大约 20% 的效率。collections.deque
诚然不熟悉其技术细节。损失 10-30%,具体取决于实施情况- 与其他迭代器助手类似的结果,如
map
/join
- 与其他迭代器助手类似的结果,如
numpy.array
也诚然对一般的numpy基本一无所知。与 deque 相似的结果
当我将 multiprocessing
似乎遇到了瓶颈。实际上损失了大约 10%,这看起来很疯狂,因为通常我可以懒洋洋地抛出问题。pixels_iter
再次 分组为每个线程的相同大小的子列表,因此new_pixels
连接了 8 个大列表而不是几百万个小列表,我认为这是快点。试图重试这个,因为我可能在凌晨 4 点的实施中以某种方式搞砸了它。- 理论上,它也可以通过保存图像缓冲区的多个小部分来避免完全连接到
new_pixels
,但这会大大增加其他地方的代码复杂性。
- 理论上,它也可以通过保存图像缓冲区的多个小部分来避免完全连接到
- 将
pixels
本身转换为bytearray
并使用切片范围就地修改它。损失了 ~30%,但内存使用量也减少了一半。
pool.map
结果附加到 new_pixels
时,像 Pypy 这样完全独立的解释器不在 table,因为我不是捆绑 Python 版本的人。
如果使用得当,NumPy 产生的结果应该比手动循环快得多。正确使用它意味着在整个数组上使用 NumPy 操作,而不仅仅是在 NumPy 数组上手动循环。
例如,
new_pixels = bytearray(pixels)
as_numpy = numpy.frombuffer(new_pixels, dtype=float)
as_numpy[1::4] *= 1 - as_numpy[::4] / 100
现在 new_pixels
包含调整后的值。