Re: C++ to Python and returning members of a C++ object

Jake <[email protected]>
Newsgroups gmane.comp.programming.swig
Message-ID <CAGcO-TaCODo1vy-kJ1h2vp6g8xqotL_cyJFzmnz2Y9BuombO=A@mail.gmail.com>
For the mailing list I wasn't able to include a zip attachment, so the
basis of my testing is this:

struct MyObject {
MyObject() = default;
MyObject(std::string s) : s(std::move(s)) { }
std::string s = "MyObject";
};

struct Mixed {
MyObject obj;
int i;
int& getI() { return i; }
const int& getIConst() const { return i; }
MyObject& getObject() { return obj; }
const MyObject& getObjectConst() const { return obj; }
};


On Fri, Aug 14, 2020 at 6:33 PM Jake <[email protected]> wrote:

> First a TLDR:
>
> - Back-ref costs 46% more CPU than your ref count scheme; ref count scheme
> costs 8 bytes memory per swig-wrapped object even when not needed.
> - Dropping the traits check adds run-time cost to cases where counting is
> not needed.  Run-time overhead is about 12%, traits check has no overhead
> in these cases.
> - Traits and your ref count scheme could be combined, but the traits are
> difficult to figure out.  Maybe we have to eat the run-time check cost for
> member access and member functions in the more general case.
>
>
> On Wed, Aug 12, 2020 at 8:24 AM Terry Barnaby <[email protected]> wrote:
>
>> Hi Jake,
>>
>> Thanks for the info. I have spent a bit of time trying to understand how
>> that typemap with "reference_container_owner" fragment and swig::traits
>> works and have tried to implement a referenced counted class member return
>> system with it. Now I may be being stupid here but what I see is:
>>
>> 1. The %typemap ret or out is used in all cases when a value/object is
>> "returned" from C++ to Python. So it is used not only when an objects
>> member is returned by reference/pointer but also when a C++ function
>> returns a value/object by reference/pointer. To handle the case of
>> incrementing the reference of a container object only when one of its data
>> members is returned something like a %typemap memberout and/or memberret
>> needs to be implemented so that it is only used when returning data members
>> by reference/pointer.
>>
>
> Yes, for the member case I think a memberret typemap support would be
> best.  The reason the container patch uses 'ret' and not 'out' is because
> we don't want to change how the value gets handled, just to add on the
> extra code after whatever already happens due to 'out'.  The container
> patch is fine using 'ret' because the placement in the body of the
> container limits the scope where it matches so it will not trigger on free
> functions elsewhere.
>
> 2. The system uses the "traits" system to, at runtime, determine if the
>> return is a reference or not for all functions returning that type.
>>
>
> The type traits are template specialization, so it is a compile-time check
> not a run-time check.  If the trait is swig::value_category it calls an
> empty function which any compiler will just remove, otherwise for
> swig::pointer_category the back-reference kicks in e.g. in std::vector<Foo>
> we expect swig::traits<Foo>::category to be swig::pointer_category, but in
> std::vector<int> we expect swig::traits<int>::category to be
> swig::value_category.  At least for containers, when you have "int&
> operator[](size_t index)" and in python you say "v = container[0]" it
> copies the int, so ref counting the container is not desired.
>
> Your approach does a run-time check always, which I also started with but
> it was complained about in the PR:
> https://github.com/swig/swig/pull/1234#issuecomment-453778142
>
> Have a look at the test I attached.  If you look at the generated member
> out code for Mixed::i, it ends like this:
>
>   arg1 = *reinterpret_cast*< Mixed * >(argp1);
>
>   result = (*int*) ((arg1)->i);
>
>   resultobj = SWIG_From_int(*static_cast*< *int* >(result));
>
>   *return* resultobj;
>
> So the int is a copy, not a pointer.  The same thing happens for
> MyObject::s, which generates:
>
>   arg1 = *reinterpret_cast*< MyObject * >(argp1);
>
>   result = (std::string *) & ((arg1)->s);
>
>   resultobj = SWIG_From_std_string(*static_cast*< std::string >(*result));
>
> But compare that to the int& freeInt() function:
>
>   result = (*int* *) &freeInt();
>
>   resultobj = SWIG_NewPointerObj(SWIG_as_voidptr(result), SWIGTYPE_p_int,
> 0 |  0 );
>
> Of course we don't want to back-ref this one anyway but I just want to
> show it's wrapped as a pointer not a value.
>
> This is a very tricky part of SWIG, but if we could figure it out between
> us then maybe we could have a general solution for containers, member
> accesses, and member functions returning non-owned pointers generally.  Or
> else it should be agreed that the runtime check of SWIG_Python_GetSwigThis
> on every access is worth avoiding the complexity of figuring out when it's
> needed based on type (or because there is no fully general type-based
> solution).  Here is why it's a pain though:
>
> Primitive types get their swig::value_category traits from
> Lib/python/std_common.i, which kicks it up to Lib/typemaps/primtypes.swg.
> Other types I haven't been able to figure out where they come from.
> However, the problem is the traits are not defined by default.  In the
> attached test code, comment out this line:
>
>  %template(StringVector) std::vector<std::string>;
>
> And you will get compiler errors because
> swig::traits<std::string>::category is not defined.  Same thing for int and
> our custom type MyObject.  Somehow the machinery for container support
> drives these trait definitions into existence when they get used as the
> value_type for a container, but I have not been able to figure out how.
> They do seem to do the right thing though, with std::string getting
> swig::value_category and MyObject getting swig::pointer_category.  Could we
> invoke this machinery somehow from a typemap to make sure the traits we
> need to check are always defined?
>
> There is another complication because int& generates wrapped pointer code
> and const int& generates copy code, e.g. comparing Mixed::getI to
> Mixed::getIConst:
>
>   arg1 = *reinterpret_cast*< Mixed * >(argp1);
>
>   result = (*int* *) &(arg1)->getI();
>
>   resultobj = SWIG_NewPointerObj(SWIG_as_voidptr(result), SWIGTYPE_p_int,
> 0 |  0 );
>
> vs
>
>   arg1 = *reinterpret_cast*< Mixed * >(argp1);
>
>   result = (*int* *) &((Mixed *const* *)arg1)->getIConst();
>
>   resultobj = SWIG_From_int(*static_cast*< *int* >(*result));
>
> But SWIG's traits system doesn't consider const-vs-non-const so either the
> typemap itself or another mechanism akin to std::is_const would be used.
> Containers seems to always use the const version so we get away with not
> making the distinction.
>
> Last issue is what happens if a user-defined typemap overrides the normal
> conversion behavior.  Typemaps as opposed to a %feature let's this be
> overridden on case-by-case basis if needed.  Downside is they could
> accidently turn off the protection on a case-by-case basis where the
> %feature is always inserting it on top of the active typemap.  An argument
> for the run-time check is we don't have to consider this at all, you just
> check whatever the typemap converts at run-time and then ref-count or not.
>
>
>> 3. The system adds a pointer to the container within the Python object
>> using Python functionality. This appears to be quite cumbersome/inefficient
>> to me for such a basic need (reference counting of used objects).
>>
>
> Your approach avoids a call to PyObject_SetAttr (some error checking, a
> call through a function pointer, a __dict__ insertion), adds a couple
> pointer checks on dereference, but increases the size of every wrapped
> object by one pointer regardless of use.  Memory for CPU efficiency seems a
> worthwhile tradeoff for common workloads.  Of course without a compile-time
> way to check whether the counting is needed you have extra overhead on
> value-types, a systematic efficiency hit.
>
> I did some benchmarking for 100 million accesses in a loop.  First here is
> the int& case where counting is not needed.  Trait check is like baseline,
> swig-this check adds about 12% overhead:
>
> $ echo "Access Mixed.i baseline" && time python3 accesstest.py
> Access Mixed.i baseline
>
> real 0m11.368s
> user 0m11.072s
> sys 0m0.277s
>
> $ echo "Access Mixed.i backref trait check" && time python3 accesstest.py
> Access Mixed.i backref trait check
>
> real 0m11.586s
> user 0m11.281s
> sys 0m0.284s
>
> $ echo "Access Mixed.i refcount check" && time python3 accesstest.py
> Access Mixed.i swigthis check
>
> real 0m12.807s
> user 0m12.504s
> sys 0m0.284s
>
> Now compare your refcount scheme against setting a back reference
> attribute, the backref has about 46% overhead over the refcount:
>
> $ echo "Access Mixed.obj refcount" && time python3 pointertest.py
> Access Mixed.obj refcount
>
> real 0m15.476s
> user 0m15.152s
> sys 0m0.293s
>
> $ echo "Access Mixed.obj backref" && time python3 pointertest.py
> Access Mixed.obj backref
>
> real 0m22.655s
> user 0m22.321s
> sys 0m0.285s
>
>
>> For information I have tidied up my small patch a little so that it is
>> enabled with %feature("containerReference") either for the whole library or
>> on a class by class basis. This appears to work fine although it could have
>> issues.
>>
>
> It segfaults on pointer-category objects because you should be passing
> "self" as the first argument not "args", I corrected that for the
> benchmarks above.
>
>
>>
>> 1. This uses the swig code to determine when a reference/pointer to an
>> objects member is being returned so handles the requirement of only being
>> used when members of objects are being returned. So none of the complex
>> traits system is needed, it isn't called unless a member is being returned
>> by reference/pointer.
>>
>
> See above for runtime overhead on value types.
>
>
>>
>> 2. A C++ pointer to the container class is added to the members returned
>> SwigPyObject and the container classes reference counter is efficiently
>> incremented. When a SwigPyObject is destroyed it checks the container
>> classes reference counter and if set decrements the containers reference
>> counter. The container reference pointer and reference counting is all
>> implemented in C++ and is simple, small and efficient.
>>
>> Unless I am missing something it seems to me:
>> 1. For most languages there needs to be a reference count type of system
>> to manage the lifetime of a libraries C++ objects as most of the languages
>> that SWIG produces an API for use dynamically created/destroyed objects and
>> references to these with garbage collectors. Doing this reference
>> counting/lifetime control within the C++ SWIG area is simpler and much more
>> efficient.
>>
>
> Many languages are not reference counted, they use graph reachability with
> periodic GC.  The general strategy of assigning a back-reference I think
> will always work whether ref-counted or not but the implementation will
> still be different across languages.  E.g. to support a back-ref in a
> static language like Java or C# the back-reference field needs to get
> inserted into the proxy class for the element, we can't just tack it on
> dynamically.  Anyway many languages will not have an equivalent of
> Py_INCREF.  I'm afraid it will be a lot of work to fix across all languages.
>
>
>>
>> 2. A SWIG internal reference counted container implementation could be
>> used by %typemaps such as your use for std::vector simplifying the typemap
>> (no fragments etc.) and making this much simpler and more efficient.
>> "%typemap(ret) value_type& { SwigContainerReference($self, $result); }"
>>
>
> Type trait check should be kept for efficiency of int, string, etc, but
> implementation should switch to the refcount.
>
> -Jake
>

_______________________________________________
Swig-user mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/swig-user
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.