OK, here's another tip/trick which will probably earn a lot of rebuttal. Hey, it's just
one approach to solving a problem, not THE answer for all that ails ya.
The
problem is that in some platforms (* cough * "D3"), VAR<-1> will
drag down a process as the variable gets larger. Operations on data in
the middle of large dynamic arrays have the same effect.
The cause of the problem is that to place a new attribute at the end of
a dynamic array, these specific platforms start scanning from the front
of the variable's address, and they chase the overflow chain all the
way to the end before appending the new data. Similarly, inserting or
deleting data in the middle of the block causes everything after it to
shift up or down, causing a huge series of frame updates and
re-linkages.
One quick way to alleviate some of
this pain is to use FlashBASIC, where text strings are manipulated in C
memory rather than in frame space - but exactly how it does this isn't
documented and there is another approach which can be used to help even
more.
This tip/trick is to create huge blocks in memory and operate within those blocks.
There are a couple ways to do this. First, start with a huge string:
BLOCK = SPACE(10000000) ; * 10MB
There is a one-time performance hit for creating that block as new frames are pulled from local or system overflow and linked.
After that, rather than using VAR<-1>, manipulate substrings within that block:
* Old way:
BLOCK = ""
... * add new attribute
BLOCK<-1> = NEW.DATA
* New way
BLOCK = SPACE(10000000)
BLOCK.SIZE = 0
... * add new attribute
DATA.SIZE = LEN(NEW.DATA) + 1
BLOCK[ BLOCK.SIZE+1 , DATA.SIZE ] = NEW.DATA : @AM
BLOCK.SIZE += DATA.SIZE
The actual size of the block hasn't changed, we've only updated the pointer to the end of the block.
I
can't explain the internals of why higher BLOCK.SIZE values don't cause
the same sort of performance pain we see when it chases to the end of
the dynamic memory. I think there are at least these two factors:
1) It's just moving forward for a count of bytes, not doing a scan for the last attribute mark. (BAL: MOV vs MICD? Been a while.)
2) When it over-writes the bytes, it's not checking for new frame organization - it knows the size of the data hasn't changed.
D3
has a built-in feature where it will not directly write to the original
frames. It copies the existing frames, updates that, then replaces the
pointer to the copy, then discards the original chain. This is called
Update Protection. Some of you may recall that your system runs faster
if you turn off update protection ... and you may also remember the pain
of dealing with GFEs. This is how they "mostly" solved that problem.
Imagine how much overhead is caused by update protection here if it does
this operation for every modification to a dynamic array in a tight
loop.
I can't completely explain the mechanics
here. But I can tell you that I've used this technique to reduce the
time processing some jobs from tens of minutes down to tens of seconds.
No kidding. Point out inaccuracies in my code sample above and argue
about semantics if you will but when implemented properly this technique
offers dramatic rewards.
Here's the same technique with a different approach...
Consider
using the %functions and operating in memory rather than in frame
space. With %malloc to allocate a block of several megabytes in memory,
you're completely avoiding any operations in frame space.
When you're done with these uber-variables, just trim off the back-end:
BLOCK = BLOCK[1,BLOCK.SIZE]
When working with %functions, be sure to release the memory.
Yes,
there is a small performance hit there, and one final one for the disk
write, but so far this is a total of three such updates compared to one
in every loop.
Fortunately, more modern MV platforms don't show the same pain because they maintain a pointer to the end of the data stream. But manipulation of the middle of a large dynamic array still causes big shift of data. If you're wondering about doing that kind of manipulation in the middle of the BLOCK, it's just normal string manipulation, something like:
ORIGINAL = BLOCK[START.HERE,END.HERE]
BLOCK[START.HERE,DATA.SIZE + LEN(ORIGINAL) + 1] = NEW.DATA : ORIGINAL
For that kind of manipulation, ORIGINAL itself should be considered for the same kind of treatment as BLOCK so that it doesn't resize either when temporarily used with huge strings:
ORIGINAL[1,END.HERE-START.HERE-1] =
BLOCK[START.HERE,END.HERE] :
SPACE( LEN(ORIGINAL) - END.HERE-START.HERE-1 )
In all of the examples above, I am not at all being careful about my math. This is in part because I'm lazy and in part to accentuate a point. If your final block size is not exactly the same as the original, then the variable will shift in memory/frames and you will encounter the same performance overhead that you are trying to defeat. The test of your success is simply the timing. If you go through a lot of work, and it's not something like 60 times faster, then the math is wrong and the variables are changing sizes. You know you have it all right when the job finishes in seconds and you find yourself wondering if it even did the job. :)
As to scalability, I think I tested this up to 20MB, and I think even this started to wear down at about 15MB. Beyond this we need to ask ourselves if we're really using the right tools.
Yes, there are other tricks for doing this:
- People learned long ago that sending output into the spooler avoids performance overhead. Just change all of your VAR<-1> statements to PRINT, then copy the resulting hold entry into frame space.
- And it's worth experimenting with DIM BLOCK(10000), bulding small blocks in each element, and then using MATBUILD/MATPARSE to switch between dynamic arrays when necessary. This approach doesn't work as well with inserting data in the middle of the block.
- Rather than building a huge item in MV, it might be better
to build records at the OS level and spawn off processes that will
sequentially concatenate new data using something like "cat new >>
big". This could work really well though there is overhead with the OS level disk updates, especially in Windows if you use a lot of file handles.
And of course one could say this is all a huge hassle to implement. Yeah, I know, been there, done this. My approach was to create a subroutine that encapsulated the ADD.ATTRIBUTE functionality, and then replace all of the VAR<-1> instances with GOSUBs. It wasn't that bad once the pattern was established. I just had to keep reminding myself that the ugly code was worth the expected reward.
This post isn't intended to cover all solutions. It's specifically to focus on this one class of a solution for this one problem. Details about the spooler solution and others can be found in old CDP archives, and someone might want to refresh them into new threads here.My goal with these tips is simply to focus on one point at a time, give it its own space, and subject it to public scrutiny. So please - if you like other solutions, please post about them in their own threads so that they get the individual reviews that they deserve.
And if you have tips of your own, please do profile them in new threads in this group.
HTH
T