parametrized package or parametrization via signatures

19 views
Skip to first unread message

Grégory Vanuxem

unread,
Dec 21, 2024, 7:59:08 PM12/21/24
to fricas...@googlegroups.com
Hello folks,

I am coding a sample package that performs operations on 32bit or
64bit floats using my Julia interface (using libjulia).

I wonder what is the best way to do it, I can use parameterized
signatures or more generically via a parameterized package. The latter
is not really necessary.
Here is a sample with a parameterized package:

JuliaMachineFloatFunctions(R : JuliaMachineFloat) :
Exports == Implementation where
JF32 ==> JuliaFloat32
JF64 ==> JuliaFloat64
STR ==> String
Exports ==> with
jlApplyFunction : (STR, R) -> R
++ jlApplyFunction(func,x)
jlApplyFunction : (STR, R, R) -> R
++ jlApplyFunction(func,x,y)
jlApplyFunction : (STR, R, R, R) -> R
++ jlApplyFunction(func,x,y,z)

Implementation ==> add
import from String
if R is JF64 then
jlApplyFunction(func,a) ==
jl_dbl_function_dbl(func,a)$Lisp
jlApplyFunction(func,a, b) ==
jl_dbl_function_dbl_dbl(func,a,b)$Lisp
jlApplyFunction(func,a, b, c) ==
jl_dbl_function_dbl_dbl_dbl(func,a,b,c)$Lisp
else -- R is JF32
jlApplyFunction(func,a) ==
jl_flt_function_flt(func,a)$Lisp
jlApplyFunction(func,a, b) ==
jl_flt_function_flt_flt(func,a,b)$Lisp
jlApplyFunction(func,a, b, c) ==
jl_flt_function_flt_flt_flt(func,a,b,c)$Lisp

But I can of course do not parametrize the package and use signatures like:

jlApplyFunc : (STR, JF64) -> JF64
or
jlApplyFunc : (STR, JF32) -> JF32

and in the implementation:
jlApplyFunction(func, a : JF64) ==
jl_dbl_function_dbl(func,a)$Lisp
etc.

They are replaced (inlined) by the Lisp calls for the two coding styles.

Example with the parameterized package above (Julia needs to perform
some initialization tasks at first call):

(1) -> a:=jf64(2)

(1) 2.0
Type: JuliaFloat64
Time: 4.03 (OT) = 4.03 sec
(2) -> b:=jf32(2)

(2) 2.0
Type: JuliaFloat32
Time: 0.02 (OT) = 0.02 sec
(3) -> jlApplyFunction("sqrt",a)

(3) 1.4142135623730951
Type: JuliaFloat64
Time: 0.02 (EV) = 0.03 sec
(4) -> jlApplyFunction("sqrt",b)

(4) 1.4142135
Type: JuliaFloat32
Time: 0.01 (EV) = 0.01 sec
(5) -> % pretend SEX

(5) 1.4142135f0
Type: SExpression
Time: 0 sec

Any idea of the "best" way to do this? And the advantages and/or
inconveniences of those two coding styles?

- Greg

Grégory Vanuxem

unread,
Dec 21, 2024, 8:23:08 PM12/21/24
to fricas...@googlegroups.com
In attachment the unified diff of the two lisp files generated, the
first with precise signatures, the second uses R in signatures.
JMFF.diff

Kurt Pagani

unread,
Dec 22, 2024, 6:56:12 AM12/22/24
to FriCAS - computer algebra system
Hmm, a delicate question on which I have no reasonable answer. However, from the end user perspective it might be favourable not to parametrize because (my experience) working in the interpreter is more comfortable without package calling all the time. It may also depend on whether how often one uses one of the types only or both in a session. From the programmer's viewpoint, however, parametrizing is certainly the better option -- isn't it?  Regarding performance I couldn't say anything useful.

Waldek Hebisch

unread,
Dec 22, 2024, 1:54:59 PM12/22/24
to fricas...@googlegroups.com
For some reason you did not mention third alternative, that is
two separate non-parametrized packages. AFAICS main differences
are overload resolution and inlining. Parametrized package
makes inlinig harder (in fact normally inlining from parametrized
package is disabled). Single package puts more stress on
overload resolution, with separate packages (either non-parametrized
ones or two instances of parametrized package) it is easier to
control visiblity. Interpreter normally do not invent package
parameters, so use from interpreter is easier in non-parametric
cases. If you want to share code for both cases, then single
package makes it slightly easier.

Which of the above is more important depends on your goals.
You may notice that there is DoubleFloatVector and few similar
"DoubleFloat" domains, but no SingleFloatVector. My rationalle
was that for math computations we frequently want higher
precision, so we need DoubleFloat version. Single float could
in principle double performance in some cases, but ATM
I decided that increase in complexity is not worth it. You
may notice that those packages are non-parametrized: some
operations are quite simple and benefit a lot from inlinig,
so that was natural choice maximizing performance.

You package at first glance will have rather high overhead,
so it is not clear to me if inlinig gives you measurable
benefits.

Note: There is a bug in overload resolution in Spad compiler
which may lead to compiler missing valid combination of
arguments depending on internal order of signatures.
Your various choices affect internal order of signatures,
so you may see differences due to this bug.

--
Waldek Hebisch

Grégory Vanuxem

unread,
Dec 22, 2024, 8:44:12 PM12/22/24
to fricas...@googlegroups.com
Le dim. 22 déc. 2024 à 12:56, Kurt Pagani <nil...@gmail.com> a écrit :
>
> Hmm, a delicate question on which I have no reasonable answer. However, from the end user perspective it might be favourable not to parametrize because (my experience) working in the interpreter is more comfortable without package calling all the time. It may also depend on whether how often one uses one of the types only or both in a session. From the programmer's viewpoint, however, parametrizing is certainly the better option -- isn't it? Regarding performance I couldn't say anything useful.

Hello,

In the example given the domain is parametrized and that works without
using package call like foo()JuliaMachineFloatFunctions(JF32|JF64)
but, it is needed, yes, if there are no JF32 or JF64 args to
jlApplyFunction-s, that's normal and annoying ;). In fact I am testing
right now without package parameters and clearly defined signatures,
that's easier.

From the programmer point of view, my principal concern is to
automatically use correct parameters before calling Lisp. First this
avoid $Lisp call, and be sure to not fall in the debugger or
completely kill the process because of segmentation errors or other
things like that. So, yes, I think so also, using specialized
signatures is more practical for the users and for me if I want
to add a special function without parameters for example.

- Greg
> --
> You received this message because you are subscribed to the Google Groups "FriCAS - computer algebra system" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to fricas-devel...@googlegroups.com.
> To view this discussion visit https://groups.google.com/d/msgid/fricas-devel/24adbee7-54aa-48d6-ac8d-449591a91ae5n%40googlegroups.com.

Grégory Vanuxem

unread,
Dec 22, 2024, 8:44:22 PM12/22/24
to fricas...@googlegroups.com
Le dim. 22 déc. 2024 à 19:55, Waldek Hebisch <de...@fricas.org> a écrit :

[snip]

> > Any idea of the "best" way to do this? And the advantages and/or
> > inconveniences of those two coding styles?
>
> For some reason you did not mention third alternative, that is
> two separate non-parametrized packages.

Hmmm... You make me think about that. In fact I did it that way for
linear algebra as I do not want to pollute the interpreter with direct
access to rarely used routines. )expose can be used for that if
necessary.

> AFAICS main differences
> are overload resolution and inlining. Parametrized package
> makes inlinig harder (in fact normally inlining from parametrized
> package is disabled). Single package puts more stress on
> overload resolution, with separate packages (either non-parametrized
> ones or two instances of parametrized package) it is easier to
> control visiblity. Interpreter normally do not invent package
> parameters, so use from interpreter is easier in non-parametric
> cases. If you want to share code for both cases, then single
> package makes it slightly easier.

Ok, thanks, that's interesting. About optimization, actually I have no
idea how to measure internal elementary operations, which ones, and
time consumed, I no longer remember in the past how I did it. With GCL
probably. I just noticed that with or without parameterization of
packages the "(DECLAIM (NOTINLINE |JuliaMachineFloatFunctions;|))".


> Which of the above is more important depends on your goals.
> You may notice that there is DoubleFloatVector and few similar
> "DoubleFloat" domains, but no SingleFloatVector. My rationalle
> was that for math computations we frequently want higher
> precision, so we need DoubleFloat version. Single float could
> in principle double performance in some cases, but ATM
> I decided that increase in complexity is not worth it. You
> may notice that those packages are non-parametrized: some
> operations are quite simple and benefit a lot from inlinig,
> so that was natural choice maximizing performance.

For DoubleFloatVector etc. I saw them a little later after I began to
code contiguous memory area for array of numbers at the Lisp level, in
fact I borrowed some of your code for Complex(JF32)|JF64) array
access. That saved me a lot of time as Complexes are implemented in
spad using cons, I borrowed that for example :)

; Complex double float vectors
; 1-based index

(defmacro jcdelt(ov oi)
(let ((v (gensym))
(i (gensym)))
`(let ((,v ,ov)
(,i ,oi))
(cons
(aref (the (simple-array double-float (*)) ,v) (- (* 2 ,i) 2))
(aref (the (simple-array double-float (*)) ,v) (1- (* 2 ,i)))))))

(slightly modified)

I also did not parameterize those domains, like you, since they are
very specialized, but your scheme is different from mine; mines are
1-based at Spad level and at lisp level I use (1- ...). (hoping some
lisp internal optimizations). And, furthemore, for 2D arrays you're
using the '(m n) scheme to specify dimensions but Clozure CL requires
a specialized vector from what I know if you want to call Fortran/C
routines functions on them and I wanted, at first, to support two CL
implementations. So I use vectors. At the end it is less optimized
code I think because of the arithmetic used for array elements access
but in terms of memory area that's the same, so they can be given
directly to BLAS and LAPACK for example. I think I will continue how I
do right now, use specialized signatures, and document the same
routines two times (Float32 and Float64). But now, with your mail, I
am thinking about separating 32/64 bits packages, that seems to me
preferable.

Last, I understand your use of only 64bit floats for mathematical
purposes (and even, from my point of view, scientific/engineering
purposes used elsewhere) but first I like the _idea_ of using just
what is needed to compute numerically what you're looking for. If I
have 127€ and 7 children, I do not need:
[joke]
(1) -> jfloat(127.0)/7

(1)
18.14285714285714285714285714285714285714285714285714285714285714285714285714
298

and neither does this:
127/7
[/joke]

I also plan to use/test 32bit computation using oneAPI from Intel and
my GPU is an on chip one, only 32 bits, I am not a gamer. After all,
graphical things... One of the advantages of using Julia is for
example the ability to use MKL instead of OpenBLAS at runtime with
just a ' jlUSing "MKL" ' with help of libstrampoline or even openAPI
with a relatively recent computer (it's a work in progress from what I
read for the support of oneAPI) .

> You package at first glance will have rather high overhead,
> so it is not clear to me if inlinig gives you measurable
> benefits.

Yes, it is just an example, it's not destined to be highly efficient,
Lisp can do that. What I want most is that if someone wants to use
some routines defined at Lisp level to call C functions via my C
wrapper (here, for Julia), use some inlined Lisp defun/defmacro but
with checks on parameters. Spad allows this very easily since it is
strongly typed. No low level errors. Function calls using $Lisp can be
dangerous... I don't think Ralf will contradict me about this.

> Note: There is a bug in overload resolution in Spad compiler
> which may lead to compiler missing valid combination of
> arguments depending on internal order of signatures.
> Your various choices affect internal order of signatures,
> so you may see differences due to this bug.

Ok, I will see maybe.

- Greg

Waldek Hebisch

unread,
Dec 22, 2024, 9:18:13 PM12/22/24
to fricas...@googlegroups.com
On Mon, Dec 23, 2024 at 02:43:43AM +0100, Grégory Vanuxem wrote:
> Le dim. 22 déc. 2024 à 19:55, Waldek Hebisch <de...@fricas.org> a écrit :
>
> > AFAICS main differences
> > are overload resolution and inlining. Parametrized package
> > makes inlinig harder (in fact normally inlining from parametrized
> > package is disabled). Single package puts more stress on
> > overload resolution, with separate packages (either non-parametrized
> > ones or two instances of parametrized package) it is easier to
> > control visiblity. Interpreter normally do not invent package
> > parameters, so use from interpreter is easier in non-parametric
> > cases. If you want to share code for both cases, then single
> > package makes it slightly easier.
>
> Ok, thanks, that's interesting. About optimization, actually I have no
> idea how to measure internal elementary operations, which ones, and
> time consumed, I no longer remember in the past how I did it. With GCL
> probably. I just noticed that with or without parameterization of
> packages the "(DECLAIM (NOTINLINE |JuliaMachineFloatFunctions;|))".

In sbcl I use sb-sprof package (included in normal FriCAS build)
to identify hot routines. For more details I look at Lisp code
or disassembly. ECL recomended general C-oriented tools, which
worked but for me was less useful than sbcl profiles. sbcl profile
shows relations between routne, its caller and called routines.
Theoretically other tools should give this info too, but somewhat
with other tools I was able only to use flat profile. Closure CL
had something to make Lisp binary look like C-one so one could
use C oriented tools. I do not think I tried this, but it would
have the same drawbacks as for ECL.
Well, I think that logic is simpler with 0 based indexing, and
optimization not always happen (in some cases 0 based indexing
gave measurably better performance).
> And, furthemore, for 2D arrays you're
> using the '(m n) scheme to specify dimensions but Clozure CL requires
> a specialized vector from what I know if you want to call Fortran/C

I do not know what Clozure CL folks say now, but when I asked
the answer was: to call non-lisp code you need to allocate special
buffer and copy data. So for Clozure CL gmp interface dully allocates
buffer and performs copy. That really does not depend much on
what is on Lisp side. Also, in Lisp there are thing like
ROW-MAJOR-AREF, so in reasonably fast Lisp there will be specialized
vector possibly behind some interface.

Using real 2D arrays causes some slowdown in Lisp access compared
to vectors. For sbcl it seem to be of order 2 (basically 2D array
indexing is less optimized than it should be), for some other
Lisp-s much bigger (2D array access may use Lisp function call
to an accessor function). But it is faster that old scheme
based on vector of vectors. I am thinking of using ROW-MAJOR-AREF
in few critical places (like matrix multiplication or vector-matrix
multiplication), that should remove most of slowdowns compared
to vectors.

--
Waldek Hebisch

Ralf Hemmecke

unread,
Dec 23, 2024, 4:15:04 AM12/23/24
to fricas...@googlegroups.com
On 12/23/24 02:43, Grégory Vanuxem wrote:
> Le dim. 22 déc. 2024 à 19:55, Waldek Hebisch <de...@fricas.org> a écrit :
>> For some reason you did not mention third alternative, that is
>> two separate non-parametrized packages.
>
> Hmmm... You make me think about that. In fact I did it that way for
> linear algebra as I do not want to pollute the interpreter with direct
> access to rarely used routines.

Why not even a 4th option?
When I look at this pattern,

JuliaMachineFloatFunctions(R : JuliaMachineFloat) :
...
jlApplyFunction : (STR, R) -> R
jlApplyFunction : (STR, R, R) -> R
jlApplyFunction : (STR, R, R, R) -> R

I am wondering why you do not export

jlApplyFunction : (STR, %) -> %

directly in JuliaMachineFloat and implement it in both
JuliaFloat32 and JuliaFloat64?

The only reason can be that you "pollute the interpreter ...".
I don't, however, see why creating a new package just for
jlApplyFunction would be a good idea, neither parametrized nor
unparametrized.

Ralf

Grégory Vanuxem

unread,
Dec 24, 2024, 1:00:01 PM12/24/24
to fricas...@googlegroups.com
Hello, 

Yes, you're right. This was a very bad example since they are already implemented. When thinking about the use of parameterized packages I omitted the fact that almost all domains I have written for Julia support have  the jlApply function with one or more parameters. This is what I did did right now, use your 4th method. For arrays this is different, some interface functions are specialized and adding these to some JuliaVector/JuliaMatrix categories does not seem to me judicious, so I will use 32 and 64 bits packages I think.

Thank you for this reply.

- Greg

 
Reply all
Reply to author
Forward
0 new messages