You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
gh-36068: Speed-up matrix construction by ensuring MatrixArgs 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
(#11589 ,
#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
#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
#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
#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
#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
#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: #36068
Reported by: Vincent Neiger
Reviewer(s): Matthias Köppe, Vincent Neiger
0 commit comments