Author: Antonio Cuni <[email protected]>
Branch: 
Changeset: r80937:cdc2cea532d1
Date: 2015-11-25 11:22 +0100
http://bitbucket.org/pypy/pypy/changeset/cdc2cea532d1/

Log:    Merge the faster-rstruct branch, which improves the performace of
        struct.unpack, which now directly reads inside the string buffer and
        directly casts the bytes to the appropriate type, when allowed.
        Unpacking of floats and doubles is about 15 times faster now, while
        for integer types it's up to ~50% faster for 64bit integers.

        This is done by the introduction of the more general
        rlib.str_storage.str_storage_getitem, which allows to read arbitrary
        types from the internal buffer of RPython strings. Right now this is
        done by casting the internal array, but eventually we should switch
        to use llop.gc_load, once it's implemented :)

diff --git a/pypy/doc/whatsnew-head.rst b/pypy/doc/whatsnew-head.rst
--- a/pypy/doc/whatsnew-head.rst
+++ b/pypy/doc/whatsnew-head.rst
@@ -22,3 +22,10 @@
 
 Fix issue #2193. ``isinstance(..., int)`` => ``isinstance(..., 
numbers.Integral)`` 
 to allow for alternate ``int``-like implementations (e.g., 
``future.types.newint``)
+
+.. branch: faster-rstruct
+
+Improve the performace of struct.unpack, which now directly reads inside the
+string buffer and directly casts the bytes to the appropriate type, when
+allowed. Unpacking of floats and doubles is about 15 times faster now, while
+for integer types it's up to ~50% faster for 64bit integers.
diff --git a/pypy/module/pypyjit/test_pypy_c/test_struct.py 
b/pypy/module/pypyjit/test_pypy_c/test_struct.py
--- a/pypy/module/pypyjit/test_pypy_c/test_struct.py
+++ b/pypy/module/pypyjit/test_pypy_c/test_struct.py
@@ -19,7 +19,8 @@
             import struct
             i = 1
             while i < n:
-                x = struct.unpack("i", struct.pack("i", i))[0]  # ID: struct
+                buf = struct.pack("i", i)       # ID: pack
+                x = struct.unpack("i", buf)[0]  # ID: unpack
                 i += x / i
             return i
 
@@ -29,7 +30,7 @@
         loop, = log.loops_by_filename(self.filepath)
         # This could, of course stand some improvement, to remove all these
         # arithmatic ops, but we've removed all the core overhead.
-        assert loop.match_by_id("struct", """
+        assert loop.match_by_id("pack", """
             guard_not_invalidated(descr=...)
             # struct.pack
             %s
@@ -40,17 +41,22 @@
             i17 = int_and(i16, 255)
             i19 = int_rshift(i16, 8)
             i20 = int_and(i19, 255)
+        """ % extra)
 
+        # the newstr and the strsetitems are because the string is forced,
+        # which is in turn because the optimizer doesn't know how to handle a
+        # getarrayitem_gc_i on a virtual string. It could be improved, but it
+        # is also true that in real life cases struct.unpack is called on
+        # strings which come from the outside, so it's a minor issue.
+        assert loop.match_by_id("unpack", """
             # struct.unpack
-            i22 = int_lshift(i14, 8)
-            i23 = int_or(i11, i22)
-            i25 = int_lshift(i17, 16)
-            i26 = int_or(i23, i25)
-            i28 = int_ge(i20, 128)
-            guard_false(i28, descr=...)
-            i30 = int_lshift(i20, 24)
-            i31 = int_or(i26, i30)
-        """ % extra)
+            p88 = newstr(4)
+            strsetitem(p88, 0, i11)
+            strsetitem(p88, 1, i14)
+            strsetitem(p88, 2, i17)
+            strsetitem(p88, 3, i20)
+            i91 = getarrayitem_gc_i(p88, 0, descr=<ArrayS 4>)
+        """)
 
     def test_struct_object(self):
         def main(n):
@@ -58,7 +64,8 @@
             s = struct.Struct("i")
             i = 1
             while i < n:
-                x = s.unpack(s.pack(i))[0]  # ID: struct
+                buf = s.pack(i)       # ID: pack
+                x = s.unpack(buf)[0]  # ID: unpack
                 i += x / i
             return i
 
@@ -66,7 +73,7 @@
         assert log.result == main(1000)
 
         loop, = log.loops_by_filename(self.filepath)
-        assert loop.match_by_id('struct', """
+        assert loop.match_by_id('pack', """
             guard_not_invalidated(descr=...)
             # struct.pack
             %s
@@ -77,14 +84,14 @@
             i17 = int_and(i16, 255)
             i19 = int_rshift(i16, 8)
             i20 = int_and(i19, 255)
+        """ % extra)
 
+        assert loop.match_by_id('unpack', """
             # struct.unpack
-            i22 = int_lshift(i14, 8)
-            i23 = int_or(i11, i22)
-            i25 = int_lshift(i17, 16)
-            i26 = int_or(i23, i25)
-            i28 = int_ge(i20, 128)
-            guard_false(i28, descr=...)
-            i30 = int_lshift(i20, 24)
-            i31 = int_or(i26, i30)
-        """ % extra)
+            p88 = newstr(4)
+            strsetitem(p88, 0, i11)
+            strsetitem(p88, 1, i14)
+            strsetitem(p88, 2, i17)
+            strsetitem(p88, 3, i20)
+            i91 = getarrayitem_gc_i(p88, 0, descr=<ArrayS 4>)
+        """)
diff --git a/pypy/module/struct/formatiterator.py 
b/pypy/module/struct/formatiterator.py
--- a/pypy/module/struct/formatiterator.py
+++ b/pypy/module/struct/formatiterator.py
@@ -149,3 +149,13 @@
     @specialize.argtype(1)
     def appendobj(self, value):
         self.result_w.append(self.space.wrap(value))
+
+    def get_pos(self):
+        return self.pos
+
+    def get_buffer_as_string_maybe(self):
+        string, pos = self.buf.as_str_and_offset_maybe()
+        return string, pos+self.pos
+
+    def skip(self, size):
+        self.read(size) # XXX, could avoid taking the slice
diff --git a/pypy/module/struct/test/test_struct.py 
b/pypy/module/struct/test/test_struct.py
--- a/pypy/module/struct/test/test_struct.py
+++ b/pypy/module/struct/test/test_struct.py
@@ -462,3 +462,29 @@
         assert self.struct.unpack_from("ii", b, 2) == (17, 42)
         b[:sz] = self.struct.pack("ii", 18, 43)
         assert self.struct.unpack_from("ii", b) == (18, 43)
+
+
+class AppTestFastPath(object):
+    spaceconfig = dict(usemodules=['struct', '__pypy__'])
+
+    def setup_class(cls):
+        from rpython.rlib.rstruct import standardfmttable
+        standardfmttable.ALLOW_SLOWPATH = False
+        #
+        cls.w_struct = cls.space.appexec([], """():
+            import struct
+            return struct
+        """)
+        cls.w_bytebuffer = cls.space.appexec([], """():
+            import __pypy__
+            return __pypy__.bytebuffer
+        """)
+
+    def teardown_class(cls):
+        from rpython.rlib.rstruct import standardfmttable
+        standardfmttable.ALLOW_SLOWPATH = True
+
+    def test_unpack_from(self):
+        buf = self.struct.pack("iii", 0, 42, 43)
+        offset = self.struct.calcsize("i")
+        assert self.struct.unpack_from("ii", buf, offset) == (42, 43)
diff --git a/rpython/jit/backend/llgraph/runner.py 
b/rpython/jit/backend/llgraph/runner.py
--- a/rpython/jit/backend/llgraph/runner.py
+++ b/rpython/jit/backend/llgraph/runner.py
@@ -638,9 +638,18 @@
         return array.getlength()
 
     def bh_getarrayitem_gc(self, a, index, descr):
-        a = support.cast_arg(lltype.Ptr(descr.A), a)
+        assert index >= 0
+        if descr.A is descr.OUTERA:
+            a = support.cast_arg(lltype.Ptr(descr.A), a)
+        else:
+            # we use rffi.cast instead of support.cast_arg because the types
+            # might not be "compatible" enough from the lltype point of
+            # view. In particular, this happens when we use
+            # str_storage_getitem, in which an rpy_string is casted to
+            # rpy_string_as_Signed (or similar)
+            a = rffi.cast(lltype.Ptr(descr.OUTERA), a)
+            a = getattr(a, descr.OUTERA._arrayfld)
         array = a._obj
-        assert index >= 0
         return support.cast_result(descr.A.OF, array.getitem(index))
 
     bh_getarrayitem_gc_pure_i = bh_getarrayitem_gc
diff --git a/rpython/jit/backend/x86/test/test_strstorage.py 
b/rpython/jit/backend/x86/test/test_strstorage.py
new file mode 100644
--- /dev/null
+++ b/rpython/jit/backend/x86/test/test_strstorage.py
@@ -0,0 +1,8 @@
+from rpython.jit.backend.x86.test.test_basic import Jit386Mixin
+from rpython.jit.metainterp.test.test_strstorage import TestStrStorage as 
_TestStrStorage
+
+
+class TestStrStorage(Jit386Mixin, _TestStrStorage):
+    # for the individual tests see
+    # ====> ../../../metainterp/test/test_strstorage.py
+    pass
diff --git a/rpython/jit/codewriter/jtransform.py 
b/rpython/jit/codewriter/jtransform.py
--- a/rpython/jit/codewriter/jtransform.py
+++ b/rpython/jit/codewriter/jtransform.py
@@ -1008,12 +1008,11 @@
             return SpaceOperation('getarrayitem_gc_i',
                                   [op.args[0], v_index, bytearraydescr],
                                   op.result)
-        else:
+        elif op.result.concretetype is lltype.Void:
+            return
+        elif isinstance(op.args[0].concretetype.TO, lltype.GcArray):
+            # special-case 1: GcArray of Struct
             v_inst, v_index, c_field = op.args
-            if op.result.concretetype is lltype.Void:
-                return
-            # only GcArray of Struct supported
-            assert isinstance(v_inst.concretetype.TO, lltype.GcArray)
             STRUCT = v_inst.concretetype.TO.OF
             assert isinstance(STRUCT, lltype.Struct)
             descr = self.cpu.interiorfielddescrof(v_inst.concretetype.TO,
@@ -1022,6 +1021,20 @@
             kind = getkind(op.result.concretetype)[0]
             return SpaceOperation('getinteriorfield_gc_%s' % kind, args,
                                   op.result)
+        elif isinstance(op.args[0].concretetype.TO, lltype.GcStruct):
+            # special-case 2: GcStruct with Array field
+            v_inst, c_field, v_index = op.args
+            STRUCT = v_inst.concretetype.TO
+            ARRAY = getattr(STRUCT, c_field.value)
+            assert isinstance(ARRAY, lltype.Array)
+            arraydescr = self.cpu.arraydescrof(STRUCT)
+            kind = getkind(op.result.concretetype)[0]
+            assert kind in ('i', 'f')
+            return SpaceOperation('getarrayitem_gc_%s' % kind,
+                                  [op.args[0], v_index, arraydescr],
+                                  op.result)
+        else:
+            assert False, 'not supported'
 
     def rewrite_op_setinteriorfield(self, op):
         assert len(op.args) == 4
@@ -1130,10 +1143,13 @@
     def rewrite_op_force_cast(self, op):
         v_arg = op.args[0]
         v_result = op.result
-        assert not self._is_gc(v_arg)
-
         if v_arg.concretetype == v_result.concretetype:
             return
+        elif self._is_gc(v_arg) and self._is_gc(v_result):
+            # cast from GC to GC is always fine
+            return
+        else:
+            assert not self._is_gc(v_arg)
 
         float_arg = v_arg.concretetype in [lltype.Float, lltype.SingleFloat]
         float_res = v_result.concretetype in [lltype.Float, lltype.SingleFloat]
diff --git a/rpython/jit/metainterp/optimizeopt/heap.py 
b/rpython/jit/metainterp/optimizeopt/heap.py
--- a/rpython/jit/metainterp/optimizeopt/heap.py
+++ b/rpython/jit/metainterp/optimizeopt/heap.py
@@ -535,10 +535,13 @@
         cf.do_setfield(self, op)
 
     def optimize_GETARRAYITEM_GC_I(self, op):
+        # When using str_storage_getitem we op.getarg(0) is a string, NOT an
+        # array. In that case, we cannot cache the getarrayitem as if it were
+        # an array, obviously
         arrayinfo = self.ensure_ptr_info_arg0(op)
         indexb = self.getintbound(op.getarg(1))
         cf = None
-        if indexb.is_constant():
+        if indexb.is_constant() and not arrayinfo.is_vstring():
             index = indexb.getint()
             arrayinfo.getlenbound(None).make_gt_const(index)
             # use the cache on (arraydescr, index), which is a constant
@@ -555,7 +558,7 @@
         self.make_nonnull(op.getarg(0))
         self.emit_operation(op)
         # the remember the result of reading the array item
-        if cf is not None:
+        if cf is not None and not arrayinfo.is_vstring():
             arrayinfo.setitem(op.getdescr(), indexb.getint(),
                               self.get_box_replacement(op.getarg(0)),
                               self.get_box_replacement(op), cf,
diff --git a/rpython/jit/metainterp/optimizeopt/info.py 
b/rpython/jit/metainterp/optimizeopt/info.py
--- a/rpython/jit/metainterp/optimizeopt/info.py
+++ b/rpython/jit/metainterp/optimizeopt/info.py
@@ -24,6 +24,9 @@
     def is_virtual(self):
         return False
 
+    def is_vstring(self):
+        return False
+
     def is_precise(self):
         return False
 
diff --git a/rpython/jit/metainterp/optimizeopt/virtualize.py 
b/rpython/jit/metainterp/optimizeopt/virtualize.py
--- a/rpython/jit/metainterp/optimizeopt/virtualize.py
+++ b/rpython/jit/metainterp/optimizeopt/virtualize.py
@@ -271,8 +271,10 @@
             self.emit_operation(op)
 
     def optimize_GETARRAYITEM_GC_I(self, op):
+        # When using str_storage_getitem we op.getarg(0) is a string, NOT an
+        # array, hence the check. In that case, it will be forced
         opinfo = self.getptrinfo(op.getarg(0))
-        if opinfo and opinfo.is_virtual():
+        if opinfo and opinfo.is_virtual() and not opinfo.is_vstring():
             indexbox = self.get_constant_box(op.getarg(1))
             if indexbox is not None:
                 item = opinfo.getitem(op.getdescr(), indexbox.getint())
diff --git a/rpython/jit/metainterp/optimizeopt/vstring.py 
b/rpython/jit/metainterp/optimizeopt/vstring.py
--- a/rpython/jit/metainterp/optimizeopt/vstring.py
+++ b/rpython/jit/metainterp/optimizeopt/vstring.py
@@ -62,6 +62,9 @@
         self.mode = mode
         self.length = length
 
+    def is_vstring(self):
+        return True
+
     def getlenbound(self, mode):
         from rpython.jit.metainterp.optimizeopt import intutils
 
diff --git a/rpython/jit/metainterp/test/test_strstorage.py 
b/rpython/jit/metainterp/test/test_strstorage.py
new file mode 100644
--- /dev/null
+++ b/rpython/jit/metainterp/test/test_strstorage.py
@@ -0,0 +1,53 @@
+import py
+import sys
+import struct
+from rpython.rtyper.lltypesystem import lltype, rffi
+from rpython.rlib.strstorage import str_storage_getitem
+from rpython.rlib.test.test_strstorage import BaseStrStorageTest
+from rpython.jit.codewriter import longlong
+from rpython.jit.metainterp.history import getkind
+from rpython.jit.metainterp.test.support import LLJitMixin
+
+class TestStrStorage(BaseStrStorageTest, LLJitMixin):
+
+    # for the individual tests see
+    # ====> ../../../rlib/test/test_strstorage.py
+
+    def str_storage_getitem(self, TYPE, buf, offset):
+        def f():
+            return str_storage_getitem(TYPE, buf, offset)
+        res = self.interp_operations(f, [], supports_singlefloats=True)
+        #
+        kind = getkind(TYPE)[0] # 'i' or 'f'
+        self.check_operations_history({'getarrayitem_gc_%s' % kind: 1,
+                                       'finish': 1})
+        #
+        if TYPE == lltype.SingleFloat:
+            # interp_operations returns the int version of r_singlefloat, but
+            # our tests expects to receive an r_singlefloat: let's convert it
+            # back!
+            return longlong.int2singlefloat(res)
+        return res
+
+    def str_storage_supported(self, TYPE):
+        py.test.skip('this is not a JIT test')
+
+    def test_force_virtual_str_storage(self):
+        byteorder = sys.byteorder
+        size = rffi.sizeof(lltype.Signed)
+        def f(val):
+            if byteorder == 'little':
+                x = chr(val) + '\x00'*(size-1)
+            else:
+                x = '\x00'*(size-1) + chr(val)
+            return str_storage_getitem(lltype.Signed, x, 0)
+        res = self.interp_operations(f, [42], supports_singlefloats=True)
+        assert res == 42
+        self.check_operations_history({
+            'newstr': 1,              # str forcing
+            'strsetitem': 1,          # str forcing
+            'call_pure_r': 1,         # str forcing (copystrcontent)
+            'guard_no_exception': 1,  # str forcing
+            'getarrayitem_gc_i': 1,   # str_storage_getitem
+            'finish': 1
+            })
diff --git a/rpython/rlib/buffer.py b/rpython/rlib/buffer.py
--- a/rpython/rlib/buffer.py
+++ b/rpython/rlib/buffer.py
@@ -22,6 +22,14 @@
         # May be overridden.
         return self.getslice(0, self.getlength(), 1, self.getlength())
 
+    def as_str_and_offset_maybe(self):
+        """
+        If the buffer is backed by a string, return a pair (string, offset), 
where
+        offset is the offset inside the string where the buffer start.
+        Else, return (None, 0).
+        """
+        return None, 0
+
     def getitem(self, index):
         "Returns the index'th character in the buffer."
         raise NotImplementedError   # Must be overriden.  No bounds checks.
@@ -66,6 +74,9 @@
     def as_str(self):
         return self.value
 
+    def as_str_and_offset_maybe(self):
+        return self.value, 0
+
     def getitem(self, index):
         return self.value[index]
 
@@ -99,6 +110,12 @@
         else:
             return 0
 
+    def as_str_and_offset_maybe(self):
+        string, offset = self.buffer.as_str_and_offset_maybe()
+        if string is not None:
+            return string, offset+self.offset
+        return None, 0
+
     def getitem(self, index):
         return self.buffer.getitem(self.offset + index)
 
diff --git a/rpython/rlib/rstruct/nativefmttable.py 
b/rpython/rlib/rstruct/nativefmttable.py
--- a/rpython/rlib/rstruct/nativefmttable.py
+++ b/rpython/rlib/rstruct/nativefmttable.py
@@ -8,15 +8,15 @@
 from rpython.rlib.objectmodel import specialize
 from rpython.rlib.rarithmetic import r_singlefloat, widen
 from rpython.rlib.rstruct import standardfmttable as std
+from rpython.rlib.rstruct.standardfmttable import native_is_bigendian
 from rpython.rlib.rstruct.error import StructError
 from rpython.rlib.unroll import unrolling_iterable
+from rpython.rlib.strstorage import str_storage_getitem
 from rpython.rtyper.lltypesystem import lltype, rffi
 from rpython.rtyper.tool import rffi_platform
 from rpython.translator.tool.cbuild import ExternalCompilationInfo
 
 
-native_is_bigendian = struct.pack("=i", 1) == struct.pack(">i", 1)
-
 native_fmttable = {
     'x': std.standard_fmttable['x'],
     'c': std.standard_fmttable['c'],
@@ -27,9 +27,6 @@
 # ____________________________________________________________
 
 
-double_buf = lltype.malloc(rffi.DOUBLEP.TO, 1, flavor='raw', immortal=True)
-float_buf = lltype.malloc(rffi.FLOATP.TO, 1, flavor='raw', immortal=True)
-
 range_8_unroll = unrolling_iterable(list(reversed(range(8))))
 range_4_unroll = unrolling_iterable(list(reversed(range(4))))
 
@@ -45,14 +42,6 @@
             fmtiter.result.append(chr(value & 0xff))
             value >>= 8
 
[email protected](0)
-def unpack_double(fmtiter):
-    input = fmtiter.read(sizeof_double)
-    p = rffi.cast(rffi.CCHARP, double_buf)
-    for i in range(sizeof_double):
-        p[i] = input[i]
-    doubleval = double_buf[0]
-    fmtiter.appendobj(doubleval)
 
 def pack_float(fmtiter):
     doubleval = fmtiter.accept_float_arg()
@@ -68,16 +57,6 @@
             fmtiter.result.append(chr(value & 0xff))
             value >>= 8
 
[email protected](0)
-def unpack_float(fmtiter):
-    input = fmtiter.read(sizeof_float)
-    p = rffi.cast(rffi.CCHARP, float_buf)
-    for i in range(sizeof_float):
-        p[i] = input[i]
-    floatval = float_buf[0]
-    doubleval = float(floatval)
-    fmtiter.appendobj(doubleval)
-
 # ____________________________________________________________
 #
 # Use rffi_platform to get the native sizes and alignments from the C compiler
@@ -134,10 +113,10 @@
 
         if fmtchar == 'f':
             pack = pack_float
-            unpack = unpack_float
+            unpack = std.unpack_float
         elif fmtchar == 'd':
             pack = pack_double
-            unpack = unpack_double
+            unpack = std.unpack_double
         elif fmtchar == '?':
             pack = std.pack_bool
             unpack = std.unpack_bool
diff --git a/rpython/rlib/rstruct/runpack.py b/rpython/rlib/rstruct/runpack.py
--- a/rpython/rlib/rstruct/runpack.py
+++ b/rpython/rlib/rstruct/runpack.py
@@ -38,6 +38,12 @@
 
         def appendobj(self, value):
             self.value = value
+
+        def get_buffer_as_string_maybe(self):
+            return self.mr.input, self.mr.inputpos
+
+        def skip(self, size):
+            self.read(size) # XXX, could avoid taking the slice
     ReaderForPos.__name__ = 'ReaderForPos%d' % pos
     return ReaderForPos
 
@@ -88,13 +94,6 @@
         exec source.compile() in miniglobals
         self.unpack = miniglobals['unpack'] # override not-rpython version
 
-    def unpack(self, s):
-        # NOT_RPYTHON
-        res = unpack(self.fmt, s)
-        if len(res) == 1:
-            return res[0]
-        return res
-
     def _freeze_(self):
         assert self.formats
         self._create_unpacking_func()
@@ -103,6 +102,7 @@
 def create_unpacker(unpack_str):
     fmtiter = FrozenUnpackIterator(unpack_str)
     fmtiter.interpret(unpack_str)
+    assert fmtiter._freeze_()
     return fmtiter
 create_unpacker._annspecialcase_ = 'specialize:memo'
 
diff --git a/rpython/rlib/rstruct/standardfmttable.py 
b/rpython/rlib/rstruct/standardfmttable.py
--- a/rpython/rlib/rstruct/standardfmttable.py
+++ b/rpython/rlib/rstruct/standardfmttable.py
@@ -12,7 +12,12 @@
 from rpython.rlib.rstruct import ieee
 from rpython.rlib.rstruct.error import StructError, StructOverflowError
 from rpython.rlib.unroll import unrolling_iterable
+from rpython.rlib.strstorage import str_storage_getitem, str_storage_supported
+from rpython.rlib import rarithmetic
+from rpython.rtyper.lltypesystem import rffi
 
+native_is_bigendian = struct.pack("=i", 1) == struct.pack(">i", 1)
+native_is_ieee754 = float.__getformat__('double').startswith('IEEE')
 
 def pack_pad(fmtiter, count):
     fmtiter.result.append_multiple_char('\x00', count)
@@ -126,6 +131,24 @@
 
 # ____________________________________________________________
 
+USE_FASTPATH = True    # set to False by some tests
+ALLOW_SLOWPATH = True  # set to False by some tests
+
+class CannotUnpack(Exception):
+    pass
+
[email protected]()
+def unpack_fastpath(TYPE):
+    @specialize.argtype(0)
+    def do_unpack_fastpath(fmtiter):
+        size = rffi.sizeof(TYPE)
+        strbuf, pos = fmtiter.get_buffer_as_string_maybe()
+        if strbuf is None or pos % size != 0 or not USE_FASTPATH:
+            raise CannotUnpack
+        fmtiter.skip(size)
+        return str_storage_getitem(TYPE, strbuf, pos)
+    return do_unpack_fastpath
+
 @specialize.argtype(0)
 def unpack_pad(fmtiter, count):
     fmtiter.read(count)
@@ -153,15 +176,54 @@
         end = count
     fmtiter.appendobj(data[1:end])
 
-def make_float_unpacker(size):
+def make_ieee_unpacker(TYPE):
     @specialize.argtype(0)
-    def unpacker(fmtiter):
-        data = fmtiter.read(size)
-        fmtiter.appendobj(ieee.unpack_float(data, fmtiter.bigendian))
-    return unpacker
+    def unpack_ieee(fmtiter):
+        size = rffi.sizeof(TYPE)
+        if fmtiter.bigendian != native_is_bigendian or not native_is_ieee754:
+            # fallback to the very slow unpacking code in ieee.py
+            data = fmtiter.read(size)
+            fmtiter.appendobj(ieee.unpack_float(data, fmtiter.bigendian))
+            return
+        if not str_storage_supported(TYPE):
+            # this happens e.g. on win32 and ARM32: we cannot read the string
+            # content as an array of doubles because it's not properly
+            # aligned. But we can read a longlong and convert to float
+            assert TYPE == rffi.DOUBLE
+            assert rffi.sizeof(TYPE) == 8
+            return unpack_longlong2float(fmtiter)
+        try:
+            # fast path
+            val = unpack_fastpath(TYPE)(fmtiter)
+        except CannotUnpack:
+            # slow path, take the slice
+            input = fmtiter.read(size)
+            val = str_storage_getitem(TYPE, input, 0)
+        fmtiter.appendobj(float(val))
+    return unpack_ieee
+
[email protected](0)
+def unpack_longlong2float(fmtiter):
+    from rpython.rlib.rstruct.runpack import runpack
+    from rpython.rlib.longlong2float import longlong2float
+    s = fmtiter.read(8)
+    llval = runpack('q', s) # this is a bit recursive, I know
+    doubleval = longlong2float(llval)
+    fmtiter.appendobj(doubleval)
+
+
+unpack_double = make_ieee_unpacker(rffi.DOUBLE)
+unpack_float = make_ieee_unpacker(rffi.FLOAT)
 
 # ____________________________________________________________
 
+def get_rffi_int_type(size, signed):
+    for TYPE in rffi.platform.numbertype_to_rclass:
+        if (rffi.sizeof(TYPE) == size and
+            rarithmetic.is_signed_integer_type(TYPE) == signed):
+            return TYPE
+    raise KeyError("Cannot find an int type size=%d, signed=%d" % (size, 
signed))
+
 def make_int_unpacker(size, signed, _memo={}):
     try:
         return _memo[size, signed]
@@ -180,9 +242,30 @@
         else:
             inttype = r_ulonglong
     unroll_range_size = unrolling_iterable(range(size))
+    TYPE = get_rffi_int_type(size, signed)
+
+    @specialize.argtype(0)
+    def unpack_int_fastpath_maybe(fmtiter):
+        if fmtiter.bigendian != native_is_bigendian or not 
str_storage_supported(TYPE):
+            return False
+        try:
+            intvalue = unpack_fastpath(TYPE)(fmtiter)
+        except CannotUnpack:
+            return False
+        if not signed and size < native_int_size:
+            intvalue = rarithmetic.intmask(intvalue)
+        intvalue = inttype(intvalue)
+        fmtiter.appendobj(intvalue)
+        return True
 
     @specialize.argtype(0)
     def unpack_int(fmtiter):
+        if unpack_int_fastpath_maybe(fmtiter):
+            return
+        # slow path
+        if not ALLOW_SLOWPATH:
+            # we enter here only on some tests
+            raise ValueError("fastpath not taken :(")
         intvalue = inttype(0)
         s = fmtiter.read(size)
         idx = 0
@@ -217,9 +300,9 @@
     'p':{ 'size' : 1, 'pack' : pack_pascal, 'unpack' : unpack_pascal,
           'needcount' : True },
     'f':{ 'size' : 4, 'pack' : make_float_packer(4),
-                    'unpack' : make_float_unpacker(4)},
+                    'unpack' : unpack_float},
     'd':{ 'size' : 8, 'pack' : make_float_packer(8),
-                    'unpack' : make_float_unpacker(8)},
+                    'unpack' : unpack_double},
     '?':{ 'size' : 1, 'pack' : pack_bool, 'unpack' : unpack_bool},
     }
 
diff --git a/rpython/rlib/rstruct/test/test_runpack.py 
b/rpython/rlib/rstruct/test/test_runpack.py
--- a/rpython/rlib/rstruct/test/test_runpack.py
+++ b/rpython/rlib/rstruct/test/test_runpack.py
@@ -1,5 +1,6 @@
 from rpython.rtyper.test.tool import BaseRtypingTest
 from rpython.rlib.rstruct.runpack import runpack
+from rpython.rlib.rstruct import standardfmttable
 from rpython.rlib.rarithmetic import LONG_BIT
 import struct
 
@@ -37,3 +38,63 @@
             return runpack(">d", "testtest")
         assert fn() == struct.unpack(">d", "testtest")[0]
         assert self.interpret(fn, []) == struct.unpack(">d", "testtest")[0]
+
+    def test_native_floats(self):
+        """
+        Check the 'd' and 'f' format characters on native packing.
+        """
+        d_data = struct.pack("df", 12.34, 12.34)
+        def fn():
+            d, f = runpack("@df", d_data)
+            return d, f
+        #
+        # direct test
+        d, f = fn()
+        assert d == 12.34     # no precision lost
+        assert f != 12.34     # precision lost
+        assert abs(f - 12.34) < 1E-6
+        #
+        # translated test
+        res = self.interpret(fn, [])
+        d = res.item0
+        f = res.item1  # convert from r_singlefloat
+        assert d == 12.34     # no precision lost
+        assert f != 12.34     # precision lost
+        assert abs(f - 12.34) < 1E-6
+
+    def test_unpack_standard_little(self):
+        def unpack(fmt, data):
+            def fn():
+                return runpack(fmt, data)
+            return self.interpret(fn, [])
+        #
+        assert unpack("<i", 'DCBA') == 0x41424344
+        assert unpack("<i", '\xfd\xff\xff\xff') == -3
+        assert unpack("<i", '\x00\x00\x00\x80') == -2147483648
+        assert unpack("<I", 'DCB\x81') == 0x81424344
+        assert unpack("<q", 'HGFEDCBA') == 0x4142434445464748
+        assert unpack("<q", 'HHIJKLM\xbe') == -0x41B2B3B4B5B6B7B8
+        assert unpack("<Q", 'HGFEDCB\x81') == 0x8142434445464748
+
+    def test_unpack_standard_big(self):
+        def unpack(fmt, data):
+            def fn():
+                return runpack(fmt, data)
+            return self.interpret(fn, [])
+        #
+        assert unpack(">i", 'ABCD') == 0x41424344
+        assert unpack(">i", '\xff\xff\xff\xfd') == -3
+        assert unpack(">i", '\x80\x00\x00\x00') == -2147483648
+        assert unpack(">I", '\x81BCD') == 0x81424344
+        assert unpack(">q", 'ABCDEFGH') == 0x4142434445464748
+        assert unpack(">q", '\xbeMLKJIHH') == -0x41B2B3B4B5B6B7B8
+        assert unpack(">Q", '\x81BCDEFGH') == 0x8142434445464748
+
+
+class TestNoFastPath(TestRStruct):
+
+    def setup_method(self, meth):
+        standardfmttable.USE_FASTPATH = False
+
+    def teardown_method(self, meth):
+        standardfmttable.USE_FASTPATH = True
diff --git a/rpython/rlib/strstorage.py b/rpython/rlib/strstorage.py
new file mode 100644
--- /dev/null
+++ b/rpython/rlib/strstorage.py
@@ -0,0 +1,62 @@
+# Support for str_storage: i.e., reading primitive types out of RPython string
+#
+# There are various possible ways to implement it, however not all of them are
+# easily supported by the JIT:
+#
+#   1. use _get_raw_str_buf and cast the chars buffer to RAW_STORAGE_PTR: this
+#      works well without the JIT, but the cast to RAW_STORAGE_PTR needs to
+#      happen inside a short "no GC" section (like the one in
+#      rstr.py:copy_string_contents), which has no chance to work during
+#      tracing
+#
+#   2. use llop.raw_load: despite the name, llop.raw_load DOES support reading
+#      from GC pointers. However:
+#
+#        a. we would like to use a CompositeOffset as the offset (using the
+#           same logic as in rstr.py:_get_raw_str_buf), but this is not (yet)
+#           supported before translation: it works only if you pass an actual
+#           integer
+#
+#        b. raw_load from a GC pointer is not (yet) supported by the
+#           JIT. There are plans to introduce a gc_load operation: when it
+#           will be there, we could fix the issue above and actually use it to
+#           implement str_storage_getitem
+#
+#   3. the actual solution: cast rpy_string to a GcStruct which has the very
+#      same layout, with the only difference that its 'chars' field is no
+#      longer an Array(Char) but e.e. an Array(Signed). Then, we just need to
+#      read the appropriate index into the array
+
+from rpython.rtyper.lltypesystem import lltype, rffi, llmemory
+from rpython.rtyper.lltypesystem.rstr import STR, _get_raw_str_buf
+from rpython.rtyper.annlowlevel import llstr
+from rpython.rlib.objectmodel import specialize, we_are_translated
+
[email protected]()
+def _rpy_string_as_type(TP):
+    # sanity check that STR is actually what we think it is
+    assert STR._flds == {
+        'hash': lltype.Signed,
+        'chars': lltype.Array(lltype.Char, hints={'immutable': True})
+        }
+    STR_AS_TP = lltype.GcStruct('rpy_string_as_%s' % TP,
+                                ('hash',  lltype.Signed),
+                                ('chars', lltype.Array(TP, hints={'immutable': 
True})))
+    return STR_AS_TP
+
[email protected](0)
+def str_storage_supported(TP):
+    # on some architectures (e.g. win32 and arm32) an array of longlongs needs
+    # to be aligned at 8 bytes boundaries, so we cannot safely cast from STR
+    # to STR_AS_TP. In that case, we str_storage_getitem is simply not
+    # supported
+    return rffi.sizeof(TP) <= rffi.sizeof(lltype.Signed)
+
[email protected]()
+def str_storage_getitem(TP, s, index):
+    assert str_storage_supported(TP) # sanity check
+    STR_AS_TP = _rpy_string_as_type(TP)
+    lls = llstr(s)
+    str_as_tp = rffi.cast(lltype.Ptr(STR_AS_TP), lls)
+    index = index / rffi.sizeof(TP)
+    return str_as_tp.chars[index]
diff --git a/rpython/rlib/test/test_buffer.py b/rpython/rlib/test/test_buffer.py
--- a/rpython/rlib/test/test_buffer.py
+++ b/rpython/rlib/test/test_buffer.py
@@ -32,3 +32,16 @@
     a = RPythonAnnotator()
     s = a.build_types(func, [int])
     assert s == SomeInteger(nonneg=True)
+
+
+def test_as_str_and_offset_maybe():
+    buf = StringBuffer('hello world')
+    assert buf.as_str_and_offset_maybe() == ('hello world', 0)
+    #
+    sbuf = SubBuffer(buf, 6, 5)
+    assert sbuf.getslice(0, 5, 1, 5) == 'world'
+    assert sbuf.as_str_and_offset_maybe() == ('hello world', 6)
+    #
+    ssbuf = SubBuffer(sbuf, 3, 2)
+    assert ssbuf.getslice(0, 2, 1, 2) == 'ld'
+    assert ssbuf.as_str_and_offset_maybe() == ('hello world', 9)
diff --git a/rpython/rlib/test/test_rawstorage.py 
b/rpython/rlib/test/test_rawstorage.py
--- a/rpython/rlib/test/test_rawstorage.py
+++ b/rpython/rlib/test/test_rawstorage.py
@@ -32,7 +32,6 @@
     assert res == 3.14
     free_raw_storage(r)
 
-
 class TestRawStorage(BaseRtypingTest):
 
     def test_storage_int(self):
diff --git a/rpython/rlib/test/test_strstorage.py 
b/rpython/rlib/test/test_strstorage.py
new file mode 100644
--- /dev/null
+++ b/rpython/rlib/test/test_strstorage.py
@@ -0,0 +1,71 @@
+import py
+import sys
+import struct
+from rpython.rtyper.lltypesystem import lltype, rffi
+from rpython.rlib.strstorage import str_storage_getitem, str_storage_supported
+from rpython.rlib.rarithmetic import r_singlefloat
+from rpython.rtyper.test.tool import BaseRtypingTest
+
+IS_32BIT = (sys.maxint == 2147483647)
+
+class BaseStrStorageTest:
+
+    def test_str_getitem_supported(self):
+        if IS_32BIT:
+            expected = False
+        else:
+            expected = True
+        #
+        assert self.str_storage_supported(rffi.LONGLONG) == expected
+        assert self.str_storage_supported(rffi.DOUBLE) == expected
+
+    def test_signed(self):
+        buf = struct.pack('@ll', 42, 43)
+        size = struct.calcsize('@l')
+        assert self.str_storage_getitem(lltype.Signed, buf, 0) == 42
+        assert self.str_storage_getitem(lltype.Signed, buf, size) == 43
+
+    def test_short(self):
+        buf = struct.pack('@hh', 42, 43)
+        size = struct.calcsize('@h')
+        x = self.str_storage_getitem(rffi.SHORT, buf, 0)
+        assert int(x) == 42
+        x = self.str_storage_getitem(rffi.SHORT, buf, size)
+        assert int(x) == 43
+
+    def test_float(self):
+        if not str_storage_supported(lltype.Float):
+            py.test.skip('str_storage_getitem(lltype.Float) not supported on 
this machine')
+        buf = struct.pack('@dd', 12.3, 45.6)
+        size = struct.calcsize('@d')
+        assert self.str_storage_getitem(lltype.Float, buf, 0) == 12.3
+        assert self.str_storage_getitem(lltype.Float, buf, size) == 45.6
+
+    def test_singlefloat(self):
+        buf = struct.pack('@ff', 12.3, 45.6)
+        size = struct.calcsize('@f')
+        x = self.str_storage_getitem(lltype.SingleFloat, buf, 0)
+        assert x == r_singlefloat(12.3)
+        x = self.str_storage_getitem(lltype.SingleFloat, buf, size)
+        assert x == r_singlefloat(45.6)
+
+
+class TestDirect(BaseStrStorageTest):
+
+    def str_storage_supported(self, TYPE):
+        return str_storage_supported(TYPE)
+
+    def str_storage_getitem(self, TYPE, buf, offset):
+        return str_storage_getitem(TYPE, buf, offset)
+
+class TestRTyping(BaseStrStorageTest, BaseRtypingTest):
+
+    def str_storage_supported(self, TYPE):
+        def fn():
+            return str_storage_supported(TYPE)
+        return self.interpret(fn, [])
+
+    def str_storage_getitem(self, TYPE, buf, offset):
+        def fn(offset):
+            return str_storage_getitem(TYPE, buf, offset)
+        return self.interpret(fn, [offset])
diff --git a/rpython/rtyper/lltypesystem/rbytearray.py 
b/rpython/rtyper/lltypesystem/rbytearray.py
--- a/rpython/rtyper/lltypesystem/rbytearray.py
+++ b/rpython/rtyper/lltypesystem/rbytearray.py
@@ -8,10 +8,10 @@
 def mallocbytearray(size):
     return lltype.malloc(BYTEARRAY, size)
 
-_, _, copy_bytearray_contents = rstr._new_copy_contents_fun(BYTEARRAY, 
BYTEARRAY,
+_, _, _, copy_bytearray_contents = rstr._new_copy_contents_fun(BYTEARRAY, 
BYTEARRAY,
                                                          lltype.Char,
                                                          'bytearray')
-_, _, copy_bytearray_contents_from_str = rstr._new_copy_contents_fun(rstr.STR,
+_, _, _, copy_bytearray_contents_from_str = 
rstr._new_copy_contents_fun(rstr.STR,
                                                                   BYTEARRAY,
                                                                   lltype.Char,
                                                                   
'bytearray_from_str')
diff --git a/rpython/rtyper/lltypesystem/rstr.py 
b/rpython/rtyper/lltypesystem/rstr.py
--- a/rpython/rtyper/lltypesystem/rstr.py
+++ b/rpython/rtyper/lltypesystem/rstr.py
@@ -60,6 +60,13 @@
     @signature(types.any(), types.any(), types.int(), returns=types.any())
     @specialize.arg(0)
     def _get_raw_buf(TP, src, ofs):
+        """
+        WARNING: dragons ahead.
+        Return the address of the internal char* buffer of the low level
+        string. The return value is valid as long as no GC operation occur, so
+        you must ensure that it will be used inside a "GC safe" section, for
+        example by marking your function with @rgc.no_collect
+        """
         assert typeOf(src).TO == TP
         assert ofs >= 0
         return llmemory.cast_ptr_to_adr(src) + _str_ofs(TP, ofs)
@@ -129,11 +136,17 @@
     copy_raw_to_string = func_with_new_name(copy_raw_to_string,
                                               'copy_raw_to_%s' % name)
 
-    return copy_string_to_raw, copy_raw_to_string, copy_string_contents
+    return _get_raw_buf, copy_string_to_raw, copy_raw_to_string, 
copy_string_contents
 
-copy_string_to_raw, copy_raw_to_string, copy_string_contents = 
_new_copy_contents_fun(STR, STR, Char, 'string')
-copy_unicode_to_raw, copy_raw_to_unicode, copy_unicode_contents = 
_new_copy_contents_fun(UNICODE, UNICODE,
-                                                                    UniChar, 
'unicode')
+(_get_raw_str_buf,
+ copy_string_to_raw,
+ copy_raw_to_string,
+ copy_string_contents) = _new_copy_contents_fun(STR, STR, Char, 'string')
+
+(_get_raw_unicode_buf,
+ copy_unicode_to_raw,
+ copy_raw_to_unicode,
+ copy_unicode_contents) = _new_copy_contents_fun(UNICODE, UNICODE, UniChar, 
'unicode')
 
 CONST_STR_CACHE = WeakValueDictionary()
 CONST_UNICODE_CACHE = WeakValueDictionary()
_______________________________________________
pypy-commit mailing list
[email protected]
https://mail.python.org/mailman/listinfo/pypy-commit

Reply via email to