#11115: Allow for extension classes to inherit cached methods from their 
category
---------------------------+------------------------------------------------
   Reporter:  SimonKing    |       Owner:  jason                
       Type:  enhancement  |      Status:  needs_review         
   Priority:  major        |   Milestone:  sage-4.7             
  Component:  misc         |    Keywords:  category cython cache
     Author:  Simon King   |    Upstream:  N/A                  
   Reviewer:               |      Merged:                       
Work_issues:               |  
---------------------------+------------------------------------------------
Changes (by newvalueoldvalue):

  * status:  new => needs_review
  * author:  => Simon King


Old description:

> In order to make full use of the parent and element methods of a
> category, it should be possible to define a ''cached'' method in the
> category, so that any object of that category inherits it ''with
> caching''.
>
> Currently, it fails as follows:
> {{{
> sage: class MyCategory(Category):
> ....:     def super_categories(self):
> ....:         return [Objects()]
> ....:     class ParentMethods:
> ....:         @cached_method
> ....:         def f(self, x):
> ....:             return x^2
> ....:
> sage: cython("""from sage.structure.parent cimport Parent
> ....: cdef class MyClass(Parent): pass
> ....: """)
> sage: O = MyClass(category=MyCategory())
> sage: O.f(4)
> 16
> sage: O.f(x) is O.f(x)
> False
> }}}
>
> So, the method is inherited, but not cached.

New description:

 In order to make full use of the parent and element methods of a category,
 it should be possible to define a ''cached'' method in the category, so
 that any object of that category inherits it ''with caching''.

 Currently, it fails as follows:
 {{{
 sage: class MyCategory(Category):
 ....:     def super_categories(self):
 ....:         return [Objects()]
 ....:     class ParentMethods:
 ....:         @cached_method
 ....:         def f(self, x):
 ....:             return x^2
 ....:
 sage: cython("""from sage.structure.parent cimport Parent
 ....: cdef class MyClass(Parent): pass
 ....: """)
 sage: O = MyClass(category=MyCategory())
 sage: O.f(4)
 16
 sage: O.f(x) is O.f(x)
 False
 }}}

 So, the method is inherited, but not cached.

 Also, cached_method could be a lot faster.

 Depends on #9976

--

Comment:

 It took a bit longer than I originally expected, but I think it was worth
 it.

 '''__New Features__'''

  * An instance of a class deriving from Parent or Element can inherit a
 cached_method from its category, ''without'' breaking the cache, even if
 it does not allow attribute assignment.

  * The cached_method decorator can, to some extent, be used in Cython
 code.

  * Cached_method is a lot faster now. In fact, using the cached_method
 decorator on a Python class is ''faster'' than a hand-written cache for a
 Python method, provided that the arguments are given by name and not by
 position.

 '''__Examples__'''

 __Python__

 The following code defines a category, and a Parent class that has a
 method with a hand-written cache and a corresponding cached method
 inherited from the category:
 {{{
 from sage.all import cached_method, Category, Objects, Parent

 class MyCategory(Category):
     def super_categories(self):
         return [Objects()]
     class ParentMethods:
         @cached_method
         def cached_by_category(self, x=100, y=-1):
             return x*y

 class MyPythonClass(Parent):
     def __init__(self):
         self._cache = {}
         Parent.__init__(self, category=MyCategory())
     def cached_by_python(self, x=100, y=-1):
         try:
             return self._cache[x,y]
         except KeyError:
             out = self._cache[x,y] = x*y
         return out
 }}}

 We do some sanity tests, and then show that the cached method is faster
 than the hand-written cache, unless we provide arguments by name:
 {{{
 sage: O = MyPythonClass()
 sage: O.cached_by_category() is O.cached_by_category(100) is
 O.cached_by_category(x=100) is O.cached_by_category(100,-1)
 True
 sage: O.cached_by_python(y=1) == O.cached_by_category(y=1)
 True
 sage: O.cached_by_python() == O.cached_by_category()
 True
 sage: O.cached_by_python() is O.cached_by_python(100) is
 O.cached_by_python(x=100) is O.cached_by_python(100,-1)
 True
 }}}

 Here are timings for the hand-knitted cache:
 {{{
 sage: timeit("O.cached_by_python()", number=10^6)
 1000000 loops, best of 3: 630 ns per loop
 sage: timeit("O.cached_by_python(100)", number=10^6)
 1000000 loops, best of 3: 970 ns per loop
 sage: timeit("O.cached_by_python(y=-1)", number=10^6)
 1000000 loops, best of 3: 1.1 µs per loop
 sage: timeit("O.cached_by_python(100,-1)", number=10^6)
 1000000 loops, best of 3: 1.31 µs per loop
 }}}
 and here are the corresponding timings for the cached method inherited
 from the category:
 {{{
 sage: timeit("O.cached_by_category()", number=10^6)
 1000000 loops, best of 3: 314 ns per loop
 sage: timeit("O.cached_by_category(100)", number=10^6)
 1000000 loops, best of 3: 954 ns per loop
 sage: timeit("O.cached_by_category(y=-1)", number=10^6)
 1000000 loops, best of 3: 1.93 µs per loop
 sage: timeit("O.cached_by_category(100,-1)", number=10^6)
 1000000 loops, best of 3: 1.06 µs per loop
 }}}

 __Cython__

 You can not use arbitrary decorators in Cython. But it is now possible to
 wrap a Cython function by the cached_method and assign it as a method -
 one needs to explicitly provide its name, though. In addition, we provide
 a hand-written cache programmed in Cython.
 {{{
 from sage.structure.parent cimport Parent
 from sage.all import cached_method

 cpdef test_func(self,x=100, y=-1):
     return x*y

 cdef class MyCythonClass(Parent):
     cdef dict _cache
     def __init__(self, category):
         self._cache={}
         Parent.__init__(self,category=category)
     cached_by_decorator = cached_method(test_func,
 name="cached_by_decorator")
     cpdef cached_by_cython(self,x=100,y=-1):
         try:
             return self._cache[x,y]
         except KeyError:
             out = self._cache[x,y] = x*y
         return out
 }}}

 It is a Parent class, and thus it can inherit parent methods from a
 category. Without the patch, the cache of an inherited cached_method would
 break, but now it is fine:
 {{{
 sage: C = MyCythonClass(MyCategory())
 sage: C.cached_by_category(y=-1) is C.cached_by_category(100,-1)
 True
 sage: C.cached_by_decorator(y=-1) is C.cached_by_decorator(100,-1)
 True
 sage: C.cached_by_decorator(y=-1) == C.cached_by_category(100,-1)
 True
 }}}

 The trick is that I introduced an attribute `__cached_methods` for Parent
 and Element, in which a cached method can be stored. The cache is (since
 #8611) stored as an attribute of the bound cached method.

 While it is nice that cached_method works at all in Cython, the
 performance is not as good as I wish. Here are the times for the hand-
 knitted cache written in Cython:
 {{{
 sage: timeit("C.cached_by_cython()", number=10^6)
 1000000 loops, best of 3: 242 ns per loop
 sage: timeit("C.cached_by_cython(100)", number=10^6)
 1000000 loops, best of 3: 538 ns per loop
 sage: timeit("C.cached_by_cython(y=-1)", number=10^6)
 1000000 loops, best of 3: 750 ns per loop
 sage: timeit("C.cached_by_cython(100,-1)", number=10^6)
 1000000 loops, best of 3: 882 ns per loop
 }}}
 Here for the cached_method inherited from the category:
 {{{
 sage: timeit("C.cached_by_category()", number=10^6)
 1000000 loops, best of 3: 754 ns per loop
 sage: timeit("C.cached_by_category(100)", number=10^6)
 1000000 loops, best of 3: 1.62 µs per loop
 sage: timeit("C.cached_by_category(y=-1)", number=10^6)
 1000000 loops, best of 3: 2.77 µs per loop
 sage: timeit("C.cached_by_category(100,-1)", number=10^6)
 1000000 loops, best of 3: 1.76 µs per loop
 }}}
 And here using the decorator in Cython code:
 {{{
 sage: timeit("C.cached_by_decorator()", number=10^6)
 1000000 loops, best of 3: 421 ns per loop
 sage: timeit("C.cached_by_decorator(100)", number=10^6)
 1000000 loops, best of 3: 1.02 µs per loop
 sage: timeit("C.cached_by_decorator(y=-1)", number=10^6)
 1000000 loops, best of 3: 1.96 µs per loop
 sage: timeit("C.cached_by_decorator(100,-1)", number=10^6)
 1000000 loops, best of 3: 1.07 µs per loop
 }}}

 '''__Conclusion__'''

 The patch provides a considerable speed-up in a case where cached_method
 used before, so that cached_method is not only convenient but efficient.
 Also, it enables to use cached_method in a broader context; here, it is
 not particularly efficient, but it is convenient.

 We want that decorated (in particular, cached) methods appear nicely in
 introspection and in the reference manual. Therefore, I like to have:

 Depends on #9976

-- 
Ticket URL: <http://trac.sagemath.org/sage_trac/ticket/11115#comment:3>
Sage <http://www.sagemath.org>
Sage: Creating a Viable Open Source Alternative to Magma, Maple, Mathematica, 
and MATLAB

-- 
You received this message because you are subscribed to the Google Groups 
"sage-trac" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/sage-trac?hl=en.

Reply via email to