Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIXES #81: 400x faster slice assignment #134

Merged
merged 53 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
f7c3110
FIXES #81: 400x faster slice assignment
muayyad-alsadi Mar 5, 2023
6da7389
FIXES #81: provide faster get too
muayyad-alsadi Mar 5, 2023
f02e2d3
FIXES #81: use slice.indices
muayyad-alsadi Mar 5, 2023
b3cf058
rely on slice and add ptr_type once
muayyad-alsadi Mar 7, 2023
f13e159
add tests
muayyad-alsadi Mar 7, 2023
2f3aee6
add tests
muayyad-alsadi Mar 7, 2023
017d837
add tests
muayyad-alsadi Mar 7, 2023
4cd3b72
add tests
muayyad-alsadi Mar 7, 2023
46677f6
add tests
muayyad-alsadi Mar 7, 2023
1f8e921
add more tests
muayyad-alsadi Mar 7, 2023
1afb8d4
remove ctypes.memmove from __getitem__
muayyad-alsadi Mar 7, 2023
18f560b
fix test
muayyad-alsadi Mar 7, 2023
09d37bc
apply flake8
muayyad-alsadi Mar 7, 2023
462efc3
fix test
muayyad-alsadi Mar 7, 2023
68f76a8
fix test
muayyad-alsadi Mar 7, 2023
9676cea
fix test
muayyad-alsadi Mar 7, 2023
274d659
introduce memorySlicer
muayyad-alsadi Mar 7, 2023
45e77c0
introduce memorySlicer
muayyad-alsadi Mar 7, 2023
4ebd300
introduce memorySlicer
muayyad-alsadi Mar 7, 2023
ef85351
replace memorySlicer with read/write
muayyad-alsadi Mar 7, 2023
7e2aec2
replace memorySlicer with read/write
muayyad-alsadi Mar 7, 2023
e1393bc
replace memorySlicer with read/write
muayyad-alsadi Mar 7, 2023
62fddc2
replace memorySlicer with read/write
muayyad-alsadi Mar 7, 2023
98d5a8f
replace memorySlicer with read/write
muayyad-alsadi Mar 7, 2023
cd18aee
replace memorySlicer with read/write
muayyad-alsadi Mar 7, 2023
c158171
replace memorySlicer with read/write
muayyad-alsadi Mar 7, 2023
9084996
replace memorySlicer with read/write
muayyad-alsadi Mar 7, 2023
aa4a15f
replace memorySlicer with read/write
muayyad-alsadi Mar 7, 2023
7b3ae95
replace memorySlicer with read/write
muayyad-alsadi Mar 7, 2023
2c622a9
replace memorySlicer with read/write
muayyad-alsadi Mar 7, 2023
7a182d6
apply review feedback
muayyad-alsadi Mar 8, 2023
404ee6b
apply review feedback
muayyad-alsadi Mar 8, 2023
20c760f
apply review feedback
muayyad-alsadi Mar 8, 2023
5303605
apply review feedback
muayyad-alsadi Mar 8, 2023
959691f
apply review feedback
muayyad-alsadi Mar 8, 2023
fbf1cb7
apply review feedback
muayyad-alsadi Mar 8, 2023
3780557
apply review feedback
muayyad-alsadi Mar 8, 2023
42df875
remove stop from write
muayyad-alsadi Mar 12, 2023
d101a70
remove stop from write
muayyad-alsadi Mar 12, 2023
7ec6b05
remove stop from write
muayyad-alsadi Mar 12, 2023
de30021
remove stop from write
muayyad-alsadi Mar 12, 2023
bab36a9
remove stop from write
muayyad-alsadi Mar 12, 2023
3b985ee
remove stop from write
muayyad-alsadi Mar 12, 2023
9c15925
types union, remove useless raise, remove () from pointer and add com…
muayyad-alsadi Mar 13, 2023
86f4730
fix typing
muayyad-alsadi Mar 13, 2023
a8e0153
write no longer accept array.array, make common code in src_ptr a fun…
muayyad-alsadi Mar 15, 2023
5a0bf2b
write no longer accept array.array, make common code in src_ptr a fun…
muayyad-alsadi Mar 15, 2023
9573435
write no longer accept array.array, make common code in src_ptr a fun…
muayyad-alsadi Mar 15, 2023
86c30d6
write no longer accept array.array, make common code in src_ptr a fun…
muayyad-alsadi Mar 15, 2023
a88fb01
write no longer accept array.array, make common code in src_ptr a fun…
muayyad-alsadi Mar 15, 2023
89a6a14
write no longer accept array.array, make common code in src_ptr a fun…
muayyad-alsadi Mar 15, 2023
25edd03
write no longer accept array.array, make common code in src_ptr a fun…
muayyad-alsadi Mar 15, 2023
1640947
write no longer accept array.array, make common code in src_ptr a fun…
muayyad-alsadi Mar 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions tests/test_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,52 @@ def test_large(self):
MemoryType(Limits(0x100000000, None))
with self.assertRaises(WasmtimeError):
MemoryType(Limits(1, 0x100000000))

def test_slices(self):
store = Store()
ty = MemoryType(Limits(1, None))
memory = Memory(store, ty)
memory.grow(store, 2)
data_ptr = memory.data_ptr(store)
ba = bytearray([i for i in range(200)])
size_bytes = memory.data_len(store)
# happy cases
offset = 2048
ba_size = len(ba)
# write with start and ommit stop
memory.write(store, ba, offset)
out = memory.read(store, offset, offset + ba_size)
self.assertEqual(ba, out)
self.assertEqual(data_ptr[offset], 0)
self.assertEqual(data_ptr[offset + 1], 1)
self.assertEqual(data_ptr[offset + 199], 199)
self.assertEqual(len(memory.read(store, -10)), 10)
# write with start and stop
memory.write(store, ba, offset + ba_size)
out = memory.read(store, offset + ba_size, offset + ba_size + ba_size)
self.assertEqual(ba, out)
# assert old
self.assertEqual(data_ptr[offset], 0)
self.assertEqual(data_ptr[offset + 1], 1)
self.assertEqual(data_ptr[offset + 199], 199)
# assert new
self.assertEqual(data_ptr[offset + ba_size], 0)
self.assertEqual(data_ptr[offset + ba_size + 1], 1)
self.assertEqual(data_ptr[offset + ba_size + 199], 199)
# edge cases
# empty slices
self.assertEqual(len(memory.read(store, 0, 0)), 0)
self.assertEqual(len(memory.read(store, offset, offset)), 0)
self.assertEqual(len(memory.read(store, offset, offset - 1)), 0)
# out of bound access returns empty array similar to list slice
self.assertEqual(len(memory.read(store, size_bytes + 1)), 0)
# write empty
self.assertEqual(memory.write(store, bytearray(0), offset), 0)
self.assertEqual(memory.write(store, bytearray(b""), offset), 0)
with self.assertRaises(IndexError):
memory.write(store, ba, size_bytes)
with self.assertRaises(IndexError):
memory.write(store, ba, size_bytes - ba_size + 1)
self.assertEqual(memory.write(store, ba, -ba_size), ba_size)
out = memory.read(store, -ba_size)
self.assertEqual(ba, out)
63 changes: 63 additions & 0 deletions wasmtime/_memory.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from . import _ffi as ffi
from ctypes import *
import ctypes
import array
import typing
from wasmtime import MemoryType, WasmtimeError
from ._store import Storelike

Expand Down Expand Up @@ -62,6 +64,67 @@ def data_ptr(self, store: Storelike) -> "ctypes._Pointer[c_ubyte]":
"""
return ffi.wasmtime_memory_data(store._context, byref(self._memory))

def read(
self,
store: Storelike,
start: typing.Optional[int] = 0,
stop: typing.Optional[int] = None) -> bytearray:
"""
Reads this memory starting from `start` and up to `stop`
and returns a copy of the contents as a byte array.

The indexing behavior of this method is similar to `list[start:stop]`
where negative starts can be used to read from the end, for example.
"""
data_ptr = self.data_ptr(store)
size = self.data_len(store)
key = slice(start, stop, None)
start, stop, _ = key.indices(size)
val_size = stop - start
if val_size <= 0:
# return bytearray of size zero
return bytearray(0)
ptr_type = ctypes.c_ubyte * val_size
src_ptr = (ptr_type).from_address(ctypes.addressof(data_ptr.contents) + start)
return bytearray(src_ptr)

def write(
self,
store: Storelike,
value: typing.Any,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I said "works with mypy" better I was referring to the fact that this is listed as typing.Any. While I don't dispute "mypy runs without errors" falling back on typing.Any defeats the purpose of using type ascription in the first place.

This function does not take any type in Python. I think you provided a compelling case for it shouldn't just take bytearray, but in that case the list of supported types I think should be listed here under typing.Union[...] or something similar.

start: typing.Optional[int] = None) -> int:
"""
write a bytearray value into a possibly large slice of memory
negative start is allowed in a way similat to list slice mylist[-10:]
if value is not bytearray it will be used to construct an intermediate bytearray
return number of bytes written
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to expand this documentation to explain what happens for the raise cases below, for example writing beyond the end of memory. My impression is that it behaves differently than native Python slicing which I think would be good to call out.

"""
data_ptr = self.data_ptr(store)
size = self.data_len(store)
key = slice(start, None)
start = key.indices(size)[0]
if start >= size:
raise IndexError("index out of range")
# value must be bytearray ex. cast bytes() to bytearray
if not isinstance(value, array.array) and not isinstance(value, bytearray):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that if array.array is to be supported here it needs more work and more tests. Currently there are no tests using array.array and for any size bigger than u8 I don't believe it's correct. For example an array.array of 32-bit integers doesn't correctly copy the size of the array into memory. Furthermore endianness is a concern with array.array where I don't think that larger than 1-byte values should be supported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are totally right, should I check for value.itemsize==1 and throw ValueError or remove array.array

# value = array.array('B', value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line can be removed?

value = bytearray(value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is bytearray the best choice of conversion here? If the input is a bytes, for example, wouldn't this create a temporary copy before then copying into the linear memory?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, bytes does not work because src_ptr = ptr_type.from_buffer(value) would give

TypeError: underlying buffer is not writable

val_size = len(value)
if val_size == 0:
return val_size
# stop is exclusive
stop = start + val_size
slice_size = stop - start
if slice_size != val_size:
raise IndexError(f"mismatched value size, value of size [{val_size}] with slice of size [{slice_size}]")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that this logic is correct? Here slice_size is always equal to val_size, so this if never fires?

if stop > size:
raise IndexError("index out of range")
ptr_type = ctypes.c_ubyte * val_size
src_ptr = (ptr_type).from_buffer(value)
dst_ptr = (ptr_type).from_address(ctypes.addressof(data_ptr.contents) + start)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that the parentheses around ptr_type are necessary here.

ctypes.memmove(dst_ptr, src_ptr, val_size)
return val_size

def data_len(self, store: Storelike) -> int:
"""
Returns the raw byte length of this memory.
Expand Down