-
-
Notifications
You must be signed in to change notification settings - Fork 487
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
Accelerating the construction of matrices of type Matrix_modn_dense #35961
Comments
Using a sparse iterator as suggested is indeed speeding up the creation of the zero matrix :
This also accelerates the constructor for the matrix argument of type
However, there are improvements that still need to be made, for example regarding the creation of a diagonal matrix and the identity matrix :
Indeed we expect the timings to be closer than they are currently, in particular for |
The problem (slow identity) seems to be due to copying the zero matrix. In I've added a condition in
This does improve the timings and there is no surprising discrepancy anymore (compare this to the previous message):
I noticed that On another note: the |
Good catch, with Some tests for when to use Regarding None versus zero, it seems that the current code is not always behaving best (in these timings below, I used the sparse iterator and calloc as you suggest):
These two calls build the zero matrix using
It is expected for the last one, but for the two others one could expect the same efficiency as above when using
(in special.py, 994) into something with None instead of 0, since all matrix spaces are supposedly supporting None and depending on the matrix space this should either not change performances, or improve them. |
For the issue of slowing down copy when using
and then, in
|
I also suggest to add In the methods mentioned above, some arguments given to
Is there any reason for them to be different ? I tried replacing the |
Yes, indeed. There also may be other places where this
I do not think there is a difference since these arguments don't play a role in the initialization/allocation. Using consistently the same one (e.g. |
… type MA_ENTRIES_ZERO There are many ways to specify entries when creating a matrix, which are handled in `args.pyx` with `MatrixArgs` class. It has been discussed before why allowing `entries=None` in the matrix creation methods can be beneficial in terms of performance (sagemath#11589 , sagemath#12020). This input `entries=None` is required to yield the zero matrix. For example: `matrix(QQ, 4, 4)` creates the zero matrix. This corresponds to the type `MA_ENTRIES_ZERO` in `args.pyx`. When one passes a value, e.g. `matrix(QQ, 4, 4, 2)`, then one gets a diagonal matrix with `2` on the diagonal; this corresponds to the type `MA_ENTRIES_SCALAR` in `args.pyx`. Currently, doing something like `matrix(QQ, 4, 4, 0)` will pick `MA_ENTRIES_SCALAR`, and therefore will build the matrix and fill the diagonal with zero. [Behind the scenes, there is still some acknowledgement that this is not the usual scalar matrix case, since this will not fail if the matrix is not square (`matrix(QQ, 4, 5, 0)` will not fail, but `matrix(QQ, 4, 5, 1)` will). But this is still not seen as `MA_ENTRIES_ZERO`.] This PR ensures the type `MA_ENTRIES_ZERO` is picked in this situation. This improves performance and solves some issues, as noted below. This PR also fixes the related issue sagemath#36065 . In fact, `entries=None` is the default in the `__init__` of all matrix subclasses presently implemented. It also used to be the default when constructing a matrix by "calling" a matrix space, until sagemath#31078 and https://github.com/sag emath/sage/commit/cef613a0a57b85c1ebc5747185213ae4f5ec35f2 which changed this default value from `None` to `0`, bringing some performance regression. Regarding this "calling the matrix space", this PR also solves the performance issue noted in sagemath#35961 (comment) , where it was observed that in the following piece of code: ``` sage: space = MatrixSpace(GF(9001), 10000, 10000) sage: %timeit zmat = space.matrix() 18.3 µs ± 130 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each) sage: %timeit zmat = space() 12.1 ms ± 65.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) ``` the called default is not the same. `space.matrix()` directly initializes the matrix through `entries=None`, but on the other hand `space()` actually calls the constructor with `entries=0`. This performance regression comes from sagemath#31078 and https://github.com/sag emath/sage/commit/cef613a0a57b85c1ebc5747185213ae4f5ec35f2 , where the default for construction from the matrix space was changed from `None` to `0`. This cannot be easily reverted: this is now the default in the `__call__` of the `Parent` class. So this PR does not directly revert the call default to `None` somehow, but the result is very close in effect (read: the overhead is small). Unchanged: through the parent call, `0` is converted to the base ring zero, which is passed to the constructor's `entries` which is then analyzed as `MA_ENTRIES_SCALAR`. Changed: the code modifications ensure that soon enough it will be detected that this is in fact `MA_ENTRIES_ZERO`. The small overhead explains why, despite the improvements, construction with `None` is still sometimes slightly faster than with `0`. Below are some timings showing the improvements for some fields. Also, this PR merged with the improvements discussed in sagemath#35961 will make the above timings of `space.matrix()` and `space()` be the same (which means a speed-up of a factor >500 for this call `space()`...). The measurement is for construction via calling the matrix space: `None` is `space(None)`, `Empty` is `space()`, `Zero` is `space(0)`, and `Nonzero` is `space(some nonzero value)`. ``` NEW TIMES field dim None Empty Zero Nonzero GF(2) 5 2.3e-06 3.2e-06 3.6e-06 3.2e-06 GF(2) 50 2.4e-06 3.3e-06 3.6e-06 5.8e-06 GF(2) 500 3.6e-06 4.5e-06 4.8e-06 3.1e-05 GF(512) 5 2.6e-05 2.8e-05 2.9e-05 2.9e-05 GF(512) 50 2.6e-05 2.9e-05 2.9e-05 4.0e-05 GF(512) 500 3.7e-05 3.8e-05 3.9e-05 1.6e-04 QQ 5 2.2e-06 3.3e-06 3.4e-06 3.2e-06 QQ 50 8.0e-06 9.2e-06 9.4e-06 1.2e-05 QQ 500 6.1e-04 6.3e-04 6.4e-04 6.7e-04 OLD TIMES field dim None Empty Zero Nonzero GF(2) 5 2.3e-06 3.5e-06 3.6e-06 3.7e-06 GF(2) 50 2.4e-06 6.0e-06 6.1e-06 6.0e-06 GF(2) 500 3.6e-06 3.0e-05 3.0e-05 3.0e-05 GF(512) 5 2.5e-05 2.8e-05 2.9e-05 2.9e-05 GF(512) 50 2.5e-05 3.9e-05 4.0e-05 4.0e-05 GF(512) 500 3.5e-05 1.5e-04 1.5e-04 1.6e-04 QQ 5 2.2e-06 3.5e-06 3.7e-06 3.7e-06 QQ 50 7.9e-06 1.2e-05 1.2e-05 1.2e-05 QQ 500 6.4e-04 6.9e-04 6.9e-04 6.9e-04 ``` Code used for the timings: ``` time_kwds = dict(seconds=True, number=20000, repeat=7) fields = [GF(2), GF(2**9), QQ] names = ["GF(2)", "GF(512)", "QQ"] vals = [GF(2)(1), GF(2**9).gen(), 5/2] print(f"field\tdim\tNone\tEmpty\tZero\tNonzero") for field,name,val in zip(fields,names,vals): for dim in [5, 50, 500]: space = MatrixSpace(field, dim, dim) tnone = timeit("mat = space(None)", **time_kwds) tempty = timeit("mat = space()", **time_kwds) tzero = timeit("mat = space(0)", **time_kwds) tnonz = timeit("mat = space(1)", **time_kwds) print(f"{name}\t{dim}\t{tnone:.1e}\t{tempty:.1e}\t{tzero:.1e}\t{ tnonz:.1e}") ``` ### 📝 Checklist - [x] The title is concise, informative, and self-explanatory. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. - [x] I have created tests covering the changes. - [x] I have updated the documentation accordingly. ### ⌛ Dependencies URL: sagemath#36068 Reported by: Vincent Neiger Reviewer(s): Matthias Köppe, Vincent Neiger
Problem Description
This follows on from the issue #28432 , which was created in 2019 and was targetting the construction of the zero matrix from the
Matrix_modn_dense_template
(classesMatrix_modn_dense_{double,float}
). The observation was that a lot of (useless) modular reductions were performed upon creation, even though no such reduction is necessary when all the wanted entries are zero.Here the issue targets more generally the construction of matrices of this type. There are several cases where creation is obviously too slow (beyond avoidable modular reductions) and there are also some inconsistencies.
For creating the zero matrix, some examples of slowliness/inconsistencies are already given in #28432 (comment) . There one notices that several natural ways of creating the zero matrix are significantly slower than copying the cached zero matrix, and that creating a zero matrix over the rationals is much faster than in the present case (over modular integers).
Consider also the identity matrix and diagonal matrices:
Creating a diagonal matrix or the identity matrix with
space.one()
is faster than creating the the identity withspace(1)
. This is because some of them rely on copies of cached matrices, whereasspace.zero()
does not. Indeed, copying is about 200ms:Creating a dense random matrix takes as long as some of the above (very special and sparse) cases:
Proposed Solution
The constructor seems to parse all the entries of the matrix, whatever the type of matrix (
MA_ENTRIES_*
fromargs.pyx
). Many other classes (rationals, integers,GF(2)
, ..) use an iterator withsparse=True
, which should bring great improvements at least for the zero matrix and diagonal matrices. It seems this could be adapted here.A more complete study of the performance of the different methods of matrix construction (types
MA_ENTRIES_*
) should be conducted, to find out if other cases have to be tackled. Also, one should be careful not to introduce performance regressions, e.g. for some types of constructions or for small matrix sizes.Alternatives Considered
At least, one should use copies of the cached zero/identity matrices whenever this is faster.
Additional Information
No response
Is there an existing issue for this?
The text was updated successfully, but these errors were encountered: