[PATCH] Negative Transition Aggregate Functions (WIP)
I've been working on speeding up aggregate functions when used in the
context of a window's with non fixed frame heads.
Currently if the top of the window is not in a fixed position then when the
window frame moves down the aggregates must be recalculated by looping over
each record in the new scope of the window frame.
Take the following as an example:
SELECT SUM(n::int) OVER (ORDER BY n ROWS BETWEEN CURRENT ROW AND UNBOUNDED
FOLLOWING)
FROM generate_series(1,20000) g(n);
Here the frame head moves along with the current row and always finishes
with the last row in the partition or the last row in the frame if no
partitioning exists.
Currently PostgreSQL must recalculate the aggregate each time, this means
looping from the current row to the end of the frame for each row
produced... So the first row needs 20000 iterations, the 2nd row 19999, 3rd
row 19998 etc.
The above query on the unpatched head, on my laptop takes 36676 ms. With
the attached patch it takes 73 ms. So a speed increase of about 500 times
for 20,000 rows. Of course this performance gap will increase with more
rows and decrease with less rows, but it's even seeing a performance
improvement with as little as 50 rows.
The attached patch is not quite finished yet, I've not yet fully covered
SUM and AVG for all types. I just need to look into numeric a bit more
before I figure that one out.
Here is a list of things still todo:
1. Fully implement negative transition functions for SUM and AVG.
2. CREATE AGGREGATE support for user defined aggregates.
3. Documentation.
4. Look to see which other aggregates apart from COUNT, SUM and AVG that
can make use of this.
Please note that if the aggregate function does not have a negative
transition function setup, e.g MAX and MIN, then the old behaviour will
take place.
One thing that is currently on my mind is what to do when passing volatile
functions to the aggregate. Since the number of times we execute a volatile
function will much depend on the window frame options, I think we should
include some sort of warning in the documentation that "The number of times
that the expression is evaluated within the aggregate function when used in
the context of a WINDOW is undefined". The other option would be to disable
this optimisation if the aggregate expression contained a volatile
function, but doing this, to me seems a bit weird as is anyone actually
going to be depending on a volatile function being executed so many times?
If anyone can see any road blocking issues to why this optimisation cannot
be done please let me know.
Otherwise I'll continue to work on this so that it is ready for CF4.
Regards
David Rowley
Attachments:
David Rowley <dgrowleyml@gmail.com> writes:
The attached patch is not quite finished yet, I've not yet fully covered
SUM and AVG for all types.
I think you *can't* cover them for the float types; roundoff error
would mean you don't get the same answers as before.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 14 Dec 2013 15:40, "Tom Lane" <tgl@sss.pgh.pa.us> wrote:
David Rowley <dgrowleyml@gmail.com> writes:
The attached patch is not quite finished yet, I've not yet fully covered
SUM and AVG for all types.I think you *can't* cover them for the float types; roundoff error
would mean you don't get the same answers as before.
I was going to say the same thing. But then I started to wonder.... What's
so special about the answers we used to give? They are also subject to
round off and the results are already quite questionable in those cases.
On Sun, Dec 15, 2013 at 12:48 PM, Greg Stark <stark@mit.edu> wrote:
On 14 Dec 2013 15:40, "Tom Lane" <tgl@sss.pgh.pa.us> wrote:
David Rowley <dgrowleyml@gmail.com> writes:
The attached patch is not quite finished yet, I've not yet fully
covered
SUM and AVG for all types.
I think you *can't* cover them for the float types; roundoff error
would mean you don't get the same answers as before.I was going to say the same thing. But then I started to wonder.... What's
so special about the answers we used to give? They are also subject to
round off and the results are already quite questionable in those cases.
I guess they probably shouldn't be, subject to rounding / influenced by
errors from tuples that are out of scope of the aggregate context.
Though saying that it would be a shame to have this optimisation for all
but float and double. I can imagine the questions in [GENERAL].. Why is
SUM(<int>) OVER ().. fast but SUM(<float>) OVER () slow? I wonder what
other RDBMS' do here...
Regards
David Rowley
Greg Stark <stark@mit.edu> writes:
On 14 Dec 2013 15:40, "Tom Lane" <tgl@sss.pgh.pa.us> wrote:
I think you *can't* cover them for the float types; roundoff error
would mean you don't get the same answers as before.
I was going to say the same thing. But then I started to wonder.... What's
so special about the answers we used to give? They are also subject to
round off and the results are already quite questionable in those cases.
Well, we can't easily do better than the old answers, and the new ones
might be arbitrarily worse. Example: sum or average across single-row
windows ought to be exact in any case, but it might be arbitrarily wrong
with the negative-transition technique.
More generally, this is supposed to be a performance enhancement only;
it's not supposed to change the results.
This consideration also makes me question whether we should apply the
method for NUMERIC. Although in principle numeric addition/subtraction
is exact, such a sequence could leave us with a different dscale than
is returned by the existing code. I'm not sure if changing the number of
trailing zeroes is a big enough behavior change to draw complaints.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 12/14/2013 05:00 PM, Tom Lane wrote:
This consideration also makes me question whether we should apply the
method for NUMERIC. Although in principle numeric addition/subtraction
is exact, such a sequence could leave us with a different dscale than
is returned by the existing code. I'm not sure if changing the number of
trailing zeroes is a big enough behavior change to draw complaints.
If we're going to disqualify NUMERIC too, we might as well bounce the
feature. Without a fast FLOAT or NUMERIC, you've lost most of the
target audience.
I think even the FLOAT case deserves some consideration. What's the
worst-case drift? In general, folks who do aggregate operations on
FLOATs aren't expecting an exact answer, or one which is consistent
beyond a certain number of significant digits.
And Dave is right: how many bug reports would we get about "NUMERIC is
fast, but FLOAT is slow"?
--
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Import Notes
Reply to msg id not found: WMe64bdf02e6841b8053e30904cc5af1a9948e719980c50ad150bd031016b6dee9272e26a792ad78016ecb2d0a6857dd43@asav-1.01.com
On Sun, Dec 15, 2013 at 2:27 PM, Josh Berkus <josh@agliodbs.com> wrote:
If we're going to disqualify NUMERIC too, we might as well bounce the
feature. Without a fast FLOAT or NUMERIC, you've lost most of the
target audience.
I don't agree with this. I'm going with the opinion that the more types and
aggregates we can support (properly) the better. I'd rather delay
implementations of the ones which could change results than see them as a
roadblock for the ones we can implement today without this danger.
I think the feature is worth it alone if we could improve COUNT(*).
It's a bit silly to have to loop through all the tuples in a tuplestore to
see how many there are after removing one, when we already knew the count
before removing it. Something like, 10 - 1 .... ummm I dunno, let's count
again.. 1, 2, 3, 4, 5, 6, 7, 8, 9.... It's 9!! Where with this patch it's
just 10 - 1 *result*. Feels a bit like asking a kid, if you have 10 beans
and you take 1 away, how many will there be. You and I know 9, but the kid
might have to count them again. PostgreSQL counts them again.
Regards
David Rowley
--
Show quoted text
Josh Berkus
PostgreSQL Experts Inc.
http://pgexperts.com
Josh Berkus <josh@agliodbs.com> writes:
I think even the FLOAT case deserves some consideration. What's the
worst-case drift?
Complete loss of all significant digits.
The case I was considering earlier of single-row windows could be made
safe (I think) if we apply the negative transition function first, before
incorporating the new row(s). Then for example if you've got float8 1e20
followed by 1, you compute (1e20 - 1e20) + 1 and get the right answer.
It's not so good with two-row windows though:
Table correct sum of negative-transition
this + next value result
1e20 1e20 1e20 + 1 = 1e20
1 1 1e20 - 1e20 + 0 = 0
0
In general, folks who do aggregate operations on
FLOATs aren't expecting an exact answer, or one which is consistent
beyond a certain number of significant digits.
Au contraire. People who know what they're doing expect the results
to be what an IEEE float arithmetic unit would produce for the given
calculation. They know how the roundoff error ought to behave, and they
will not thank us for doing a calculation that's not the one specified.
I will grant you that there are plenty of clueless people out there
who *don't* know this, but they shouldn't be using float arithmetic
anyway.
And Dave is right: how many bug reports would we get about "NUMERIC is
fast, but FLOAT is slow"?
I've said this before, but: we can make it arbitrarily fast if we don't
have to get the right answer. I'd rather get "it's slow" complaints
than "this is the wrong answer" complaints.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
David Rowley <dgrowleyml@gmail.com> writes:
On Sun, Dec 15, 2013 at 2:27 PM, Josh Berkus <josh@agliodbs.com> wrote:
If we're going to disqualify NUMERIC too, we might as well bounce the
feature. Without a fast FLOAT or NUMERIC, you've lost most of the
target audience.
I think the feature is worth it alone if we could improve COUNT(*).
Agreed, count(*) and operations on integers are alone enough to be worth
the trouble.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I wrote:
It's not so good with two-row windows though:
Actually, carrying that example a bit further makes the point even more
forcefully:
Table correct sum of negative-transition
this + next value result
1e20 1e20 1e20 + 1 = 1e20
1 1 1e20 - 1e20 + 0 = 0
0 0 0 - 1 + 0 = -1
0 1 -1 - 0 + 1 = 0
1
Those last few answers are completely corrupt.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sun, Dec 15, 2013 at 3:08 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
I wrote:
It's not so good with two-row windows though:
Actually, carrying that example a bit further makes the point even more
forcefully:Table correct sum of negative-transition
this + next value result
1e20 1e20 1e20 + 1 = 1e20
1 1 1e20 - 1e20 + 0 = 0
0 0 0 - 1 + 0 = -1
0 1 -1 - 0 + 1 = 0
1Those last few answers are completely corrupt.
I guess the answer for the people that complain about slowness could be
that they create their own aggregate function which implements float4pl as
the trans function and float4mi as the negative trans function. They can
call it SUMFASTBUTWRONG() if they like. Perhaps it would be worth a note in
the documents for this patch?
I've removed the negative trans function setups for float4 and float8 with
SUM and AVG stuff in my working copy now.
As for numeric, I did start working on this just after I posted the
original patch and before I saw your comment about it. I did end up making
do_numeric_desperse() which was to be the reverse of do_numeric_accum(),
but I got stuck on the equivalent of when do_numeric_accum()
does mul_var(&X, &X, &X2, X.dscale * 2);
If it is decided that we don't want to implement a negative trans function
for numeric, then I guess I could leave in my trans function to allow users
to create their own fast version, maybe?
Regards
David Rowley
Show quoted text
regards, tom lane
David Rowley <dgrowleyml@gmail.com> writes:
I guess the answer for the people that complain about slowness could be
that they create their own aggregate function which implements float4pl as
the trans function and float4mi as the negative trans function. They can
call it SUMFASTBUTWRONG() if they like. Perhaps it would be worth a note in
the documents for this patch?
I think it would be an absolutely perfect documentation example to show
how to set up such an aggregate (and then point out the risks, of course).
As for numeric, I did start working on this just after I posted the
original patch and before I saw your comment about it. I did end up making
do_numeric_desperse() which was to be the reverse of do_numeric_accum(),
but I got stuck on the equivalent of when do_numeric_accum()
does mul_var(&X, &X, &X2, X.dscale * 2);
Ummm ... why doesn't it work to just use numeric_add and numeric_sub,
exactly parallel to the float case?
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sun, Dec 15, 2013 at 3:08 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
I wrote:
It's not so good with two-row windows though:
Actually, carrying that example a bit further makes the point even more
forcefully:Table correct sum of negative-transition
this + next value result
1e20 1e20 1e20 + 1 = 1e20
1 1 1e20 - 1e20 + 0 = 0
0 0 0 - 1 + 0 = -1
0 1 -1 - 0 + 1 = 0
1Those last few answers are completely corrupt.
For sake of the archives I just wanted to reproduce this...
I used the following query with the patch which was attached upthread to
confirm this:
SELECT sum(n::float8) OVER (ORDER BY i ROWS BETWEEN CURRENT ROW AND 1
FOLLOWING)
FROM (VALUES(1,1e20),(2,1)) n(i,n);
sum
--------
1e+020
0
(2 rows)
SUM(1) should equal 1 not 0.
But unpatched I get:
sum
--------
1e+020
1
(2 rows)
This discovery seems like good information to keep around, so I've added a
regression test in my local copy of the patch to try to make sure nobody
tries to add a negative trans for float or double in the future.
Regards
David Rowley
Show quoted text
regards, tom lane
On Sun, Dec 15, 2013 at 9:29 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
David Rowley <dgrowleyml@gmail.com> writes:
I guess the answer for the people that complain about slowness could be
that they create their own aggregate function which implements float4plas
the trans function and float4mi as the negative trans function. They can
call it SUMFASTBUTWRONG() if they like. Perhaps it would be worth a notein
the documents for this patch?
I think it would be an absolutely perfect documentation example to show
how to set up such an aggregate (and then point out the risks, of course).
I've attached an updated patch which includes some documentation.
I've also added support for negfunc in CREATE AGGREGATE. Hopefully that's
an ok name for the option, but if anyone has any better ideas please let
them be known.
For the checks before the aggregate is created, I put these together quite
quickly and I think I'm still missing a check to ensure that the strict
property for the trans and negtrans functions are set to the same thing,
though I do have a runtime check for this, it's likely bad to wait until
then to tell the user about it.
As for numeric, I did start working on this just after I posted the
original patch and before I saw your comment about it. I did end up
making
do_numeric_desperse() which was to be the reverse of do_numeric_accum(),
but I got stuck on the equivalent of when do_numeric_accum()
does mul_var(&X, &X, &X2, X.dscale * 2);Ummm ... why doesn't it work to just use numeric_add and numeric_sub,
exactly parallel to the float case?
I've not quite got back to this yet and I actually pulled out my initial
try at this thinking that we didn't want it because it was affecting the
scale. The transition function for SUM numeric does not seem to use
numeric_add, it uses numeric_avg_accum as the transition function which
lets do_numeric_accum do the hard work and that just does add_var. I
changes these add_var's to sub_var's and altered the initial value to flip
the sign so that NULL,10 would be -10 instead of 10. I think that's all it
needs, and I guess I leave the dscale as is in this situation then?
Regards
David Rowley
Show quoted text
regards, tom lane
Attachments:
negative_transition_funcs_v0.8.patch.gzapplication/x-gzip; name=negative_transition_funcs_v0.8.patch.gzDownload
��|�R negative_transition_funcs_v0.8.patch �kS�D���W�����(oD;"T��8���&�M�1����=�l6�m,wt�"s���9g������� ��$�������~'����LFzb:�M�k&7��W/Bz��Q��tz�D�AO�vw�6��v-��f�YO���D{���u$�����}�!��g�������Y�c�b>���F�M�s������,�yY�����$V��@�<��]@�����j�������V�Fa�R�C���?*�?��w�N���,�r���d����G�~G�><2�U�^����'%�Q5���/,�ZQa,��r��I�rZ��
�^�d�F�K���?��|�w|�;��NG�'r���k�r~R�|f�\��zq78��/��| O�Y�D�Sr)�E��O7���@@�}�X�������a�������B|������������f���U��r?�� ��&}��M�J�(o-q{~�6r&���>�hZ���/��!�����m�t~1�>�Z�dPOix=|�����{Ba�.���c�<5�������5��l���L��(bS�>Y���������oqu�w�O�����Fw?�\<OG�����E&����rp'>��F��^)��N��������mp|�i��f&�6i���������,5$��� ��"����&�O�b��I�B�["{TF���O�� E�6;�j�A������ny\���G���iQd�$��2����0��Si&�Se����qy�W�iDh��(_�b)��d�%:R���������t����[��g�/R5�O����`bK���MXK��b�����L��x����j8���w��)WREgo6���*���s�=;i�g��,��0^&���=�"�I�Z7v���Y�5v�?�C�� L����N�VMLN�E4��'�-%S��$�����)�r�Zjdzd���l<U�MV�,+2���&7W�@��g������E�^����"U1������O'����LN,����9�u��xi8f���jO�/�
\����C ��y��~k����|������������JCe
��-�)�R������A�vDVb�*'������;,����������N'88>��k��� �f)�zTt������K>MD#���N�7J"U:_�'�Eddt�_�B��&�t=7D�9]-ID��4�.[�(0�����T���s��XtR�eG���T�G�0fb�7D�<R��m#�c�e>L���H{���b�KT
XS{�[8�!��t�����{�)��x6�����_�rFj��=jMLk��aV��HfU�6z�\��*���p���$J�@H�����\����
$�c!�JA����p�fHN>����y�z�� ��`/!�6Q�~C�����`Q���5��hc�q��������
�"���W��:5����*3�2X�\�db&6�QR��_�5��2��\��4��F9��HO���Pc����lp�5(<(�y��"�`�0*�21
7��g�Aw�Dpq��Qrf���:M������U� [�~T�2�0+a���Q����K��D�T���v*�"�Xz��l���LJ�O�� �y�+�p���N�N���=�5�p�X����&��'t'�j(5<�Nn.��� 68@���K����h�����B��)�.U A��Y9��U���L��:[E$�.��S\i�&�Qo�~ [n���p�G�o�4���;>��x-������� �NGy�Q�D&��|G����%��0��3g��c�����nA�v��]-
K�
B���� �� f�Mh�Y
g\p�S�m���7�O��S��F�����U#�%3 n!�2�����Z" \%I� ���������_��q|:3f�b���2��������qiM�S]�b��q�'�i��P�UGZ�2K�\F��"�-K�7*����?�R�a<��Z�� 6��� �4�������Tf v��\�$�s ����s���n*U XZcpwws�,�O���}��7������96�F_��>��j�A�@:5��fu����s�3x�,(�CabTx��Y
ID]1ISF���X5*��T�� o��S�k��W����3u�����Q��m^bc�9Xk%��S���#7����gP���Re����a5T�lV���Q�tb���H=WV�5�h~
: W����d�v��P�x88�����~��;|��S�?��������>,�{F��N�K�@v��M���������r�-q9�\_�/~]��}}~��+c��:c+����[����IY�6��9�7q}�j�(_"������M�vd�F4���p�]{�R��"�6�}��u*?�k����?��
!�-v��
����Sx�p**q�H1�p�a�qk3�i�����E���X$,PT��� al,Mo\�����dN�����M}���z0[6���=��yw��+e�z�\�\jK�#*��p~�@8��u����:?K�� _ �I";��e�*���4L"=�>�EwtN|'�(�'FT���P���q=��v���E�
Ro�<J�}��`���K��;�~{u�q��j�����?,,[X;�}@D�f&#�������"�;%��!��,���&�����I${���S4�o^���J���X����i_��+[��~1���A'�JB�w�B��0��"/W���e��T��h���0�J\��\rb�(���B�j~]Dw+Z5�y�����$
;�)�u�c�r���i,�cu�V/����n�}���z ����<
�N�t�wtp�W"u- ����TzL�?�� 8��`�+v�%��G�Q?[�u*����gT�#��@H/<D.1xI�R �|�c��nB�Y(_����� j>Y5������A`�w
�������D�&|��z���s�������6�Z� �d�y:��l^K�iG]�%�B�g8+� [9Te]YbkA�<\u�$O�c8L'�(��(s,��u&�L��,��_�9��+�����X��t:{���;Qk��#�<����.��.{ 9>�o��� 'C�U)�� J��A���,���*�n�sl/��;Hj*���r��d/V�b�@u�Z�kR�g�N�1�r�q4D�.{�K4�i���
����W���$#S����`k�Vj������ZY���{��x�������U�>���>�Q*�>��C�Hm1c�Jl�/ i�n��;`��t�����I��E[
z�2�yG���#l���k��?�T������������������4����Z$,/�8Cs
l�g������;�����^�������o eQ���GZ��X�Q� �'{oBg�����;@����������*����i��Ms-n����ls�ZW:��!�w;|������9s[��U��[aw���}�FU
�4'��Jl��D��"���=�:���(����l��� R��5���?�����Z��C8�V~��� ��7���yY.����S]���W*�����/��QZ+*C��l�)}��:�_����!\"<����|(�����'��z�
�;�8�����3p
�|�����"(�/�xX�m����e�p7<�D���xV|;�^��Ey�Lj<=L]\�`�j�$�O>����b��-������H���'�l6����8a�$�T��QC�I�\���iL{<o�*�r�*u����9oF=s��w������SJ�T)N��������H'(���Z�)>m��yf�R��6�Z(\_�qy�`��vj������ ����\#��aX'��������<\�,�J0pWT�,Ke/�$<K��@S�QB�\I�����J��h��Yw���{�Ax��+ZZR�v�Ma��:�l��ID��p���d�A;^�O��
�-�8<�����Cr,��X�a��.������� �H��H;M<���g���K�9�8��[
��S�j����lb�����mc���%L
w�����F��kj����.�h�������K�����b�T���83^���9���?�=h�}5�*����������'���v���������b6����<h ����b�p����� �VEa0`��*�� ����l��bR�B)��%k 9������b��2'�c`���+���%;A�������w�����k�fkz���b��>2B�� ���~���R����$�O�.?���EI C�kQU�B��^`ee�!��b���Dn���I��6�
���b-�g.9>z���`2]���iT�'1w���A�s�>>��� }������8�S_����x{g5����T�gfogm�N� (��w�����3S�_P��N�.�fMD3��������h��5������hS%�j�����YZH6hl�XN%�����
����%�^K������Z�LiY!��?'s��g�`\R��a2���Q����@�dy�Q��K���Zd�$�@�S����ln(�����~3uo�3�'D��)�jSsSx�7n��4�mK�������n�'+&i1mF���:P 1a�F��N��� .�~�m�<�-�f�����X<��S�������O!���p��>��A�/K\���MW��4�~�����M���T�{�PA����������_9W�o������o���v�Sq��%�L��s� YAm��K-��W5���'�t
"���5`���� �q�����c�q��kt��2~�������X����/V�E}�{�@���I�l��[�7���
N��?j���M�+�A��^h4+k���zx/dH�/W��W�o�{���������moi�����tk�C�P�pa�LO�@�����h�H��,��K��HK�!"�������@v�K�����������@e-���85"�Q��K-�D��9� ��J�
� ���G��A/�@�b�"�LeKcK��r +>��WQ}��6LTE�ql'a�J��|�B2�N�����(����f�1�F��[�7���<�9u����Y4�S�x�
s�AC���U���9vB�4h�0w�������]��>Ju����&������H�gq��,�� #-��V�9j����fIb&��0@)\E(��_�Z�"^������)~����0!����rx5Yj���X�pj�\h�����E'��{Z:��m�fY�vM��W���WM�OfLV�4�K��L�T'RWB2�kE�"��p����2�Uo�>��H���`��(�`k��� W�K@�LA"���m{;��F{��l�'�Us�(����#�(������=t��zn���AF_:8[F���~�����>��eg���
��kk+g���O���8���%R��}�����$4bA�U.X�aYXOn�J`����� ����J� P8�����h���f��#�\���X����of���<�e�*�"��q���P�3��O�����x�Upnrc�<].6x&�!�j�2�� �����lz7����������W�8d�8�~�rIZ`���]w ���]Y�+!"d~�f�$� 3�������u8�����a�.��v�~�~O����� =��9�HS��*e��o��e�|E�uO�M0�,������-��0w�0�x[�6���������� �Z�*H���c3������,-OIdbY���� �4HD�����E�B�
Y��o�5j�lb��#m��(u�������f:��!�~�8�e�Ex���r��<�$$�W�89�5�i&i��<�����c>B\����=L&�Um/�� �QJ����v��3�u2-�w F�3� Dy��:�H�LSk8�����+�\p��mw�L/��`r�k�b]@:���P����p{|�� �'f��
�kV�u)����Dz�����=
�\I:[��H���MsL������;�l���W���l������;� 46 �1S�b��.d�B�1 �0����d���&��5a�2n
\�o��V�I��P�+�W��b=\+�!��&f0@V� ����YZ��\����U���A�� �%}�="$?��������`P�9� �[� ��m��Fn_�������i>j�1Q��J���������<A�/���
-���)����e���EN��������FL9
��{��t�����R��nRI��H�y���v�y��>7f��t�8��fh��������o=��7�S��k�b�?*x&ddkv�n����r��<���b�%�A�������$eE��
��m}3�
6\���JB\
K�j��8�H.������Q�lD5��������P#�GSs�,�f:i�
�����R��pi{�'���_��v��.��0JS��
�������<�Z����b�_=���0%��aG�y����s��Q����"�p�E����*�W�Q������KSq�^'����-3�]���������i��7ZXxQ��$�:�������w�7oF���4&��9��{^����cb�B;��BE��A�/����������r|>z�/�� �yY;T�������R8"��.w�,���&