Jump to content

ShaunR

Members
  • Posts

    4,871
  • Joined

  • Days Won

    296

Posts posted by ShaunR

  1. "a disruptor implementation that copies an element out of a buffer N times for each of N processes running in parallel"

    can ever beat

    "a single queue that dequeues an element and hands it to N processes running in parallel without making a copy"

    for any data type beyond basic numerics, and for basic numerics I doubt that the gains are particularly measurable. The data copy is just that expensive.

     

    OK. Words aren't working.

    What is your "single queue that dequeues an element and hands it to N processes running in parallel", implementation of this?: AQ1.vi

  2. LvVariant is a C++ class. There is a reference count in the variant object, but I wouldn't expect it to come into play here... can't say for certain. When a To Variant primitive executes, LV does "newVariant = new Variant()". Then newVariant.mTD is assigned the data type object -- this is an object with its own fairly complex structure itself. Then the data on the wire is top-swapped into the variant's "newVariant.mData" field (To Variant's input is a stomper so LV has already guaranteed that the data is independent of any other parallel branch and is thus free to be stomped on). There's also an mAttrs field that the attribute table, which is left as null unless/until an attribute is added to the variant.

     

    LvVariant does not have any virtual methods so there is no vTable on the C++ object. But the actual data size of the LvVariant class is (1 + sz + sz + 4 + 4 + sz) bytes, where sz is 8 or 16 depending on how many bytes a pointer has on your system. The data is in the first of those pointers. The reference count is the first of the 4 bytes. There's also a transaction count -- the second 4 byte number -- which is unused within Labview.exe and is exported apparently for use with flex data -- you can basically ignore that.

     

    You'll never be able to change out the type descriptor -- that's the final sz in the size. Those are refcounted C++ objects that you have to go through the constructors to instantiate. So you could in theory change out the data within a variant, provided you started with a variant that had the type descriptor you needed already in it. You would need to either top swap (swap, not move) the data into the variant or copy the data in, depending upon whether the source data is stompable. A variant claims right of destruction over the data pointer it contains.

     

    Don't know if any of that helps you.

    +1. Plenty for me to play with here. Ta very muchly (did you mean 4 or 8 bytes for the pointer size rather than 8 or 16?)

    If the variant claims right of destruction, what happens to the copies in the buffer? Is this the reason why it crashes on deallocation?

  3. The what? There are no locks around the private data of a class. It unbundles just the same as a cluster. I've got no idea what you're referring to here.

     

    *blink* These are not equatable in any mental construct I have in my head. I cannot think of any architecture where a storage mechanism can be substituted for a data type or vice versa.

     

    I am really and truly lost reading these posts.

    I look at it very simplistically. The private data cluster is just a variable that is locked so you can only access it via public methods. FGV is also a variable that is locked so you can only access it with the typdef methods. In my world. There is no difference between a class and an FGV as a storage mechanism.

     

     

    ShaunR is looking for ways to achieve this in LabVIEW by cheating values onto the wires that LV thinks are separate data instances but are not actually.

    I've got no idea what labview thinks (that's why your input is invaluable). I just want it to do what I need. If labview requires me to make a copy of an element so it is a data copy rather than shared, then I'm ok with that (LVCopyVariant, LVVariantGetContents, LVVarintSetContents et. al. docs would be useful ;) ). But I'm not ok with a copy of an entire array for one element that causes buffer-size dependant performance. The only reason I have gone down this path is because a global copies an entire array so I can get to one element. If you have another way without using the LVMM functions then I'm all ears. I just don't see any though (and I've tried quite a few).

    Throw me a bone here eh?

  4. Ahh I think you got that wrong. Ever used the inplace structure node? There you have an inplace replace array element without having to read and write the entire array.
    Indeed. But it seems to lock the entire array while it does it. When I experimented, it one of the slowest methods.
     

    As to SwapBlock() I can't remember the details but believe it is more or less an inplace swapping of the memory buffer contents but without any locking of any sort. As such it does more than MoveBlock() which only copies from one buffer to the other, but not fundamentally more. That API comes from the LabVIEW stoneage, when concurrent access was not an issue since all the multithreading in LabVIEW was handled through its own cooperative multithreading execution system so there was in fact no chance that two competing LabVIEW modules could attempt to work on the same memory location without the LabVIEW programmer knowing it if he cared. With preemptive multitasking you can never have this guarantee, as your SwapBlock() call could be preempted anywhere in its executation.

     

    One thing about SwapBlock could be interesting, as it does operate on buffers as 4 byte integers if both buffers are 4 byte aligned and the length to operate on is a multiple of 4 bytes.

    Ahhhh. Those were the days [misty, wavey lines]. When GPFs where what happened to C programmers and memory management was remembering what meetings to avoid. :D

    Where is this documented about SwapBlock (or anything for that matter)? I couldn't even find what arguments to use. I also found a load of variant functions (e.g. LVVariantCopy) but have no idea how to call them.

  5. Absolutely. I'm curious though, how realistic of a use case is it where the bottleneck would be the actual copying of an element from the buffer such that allowing independent tasks to simultaneously operate on different buffer elements will actually give a measurable return relative to the execution time of whatever is making these calls? I just don't now the answer to this.

    Well. You answered that in a previous post. If you remember, there was a significant change in times for different sizes of buffer-the bigger it got, the worse it was. As you pointed out. That was because the global variable array forced a copy of the entire array to access a single element. So although you are correct in that with native labview code you can "get" a single element. In reality, a copy of an entire array is required to get it (the only scenario where this isn't the case is where labview uses the "sub-array" wire). The latest version allows a buffer size as large as you want with no impact. So if you want a practical visualisation. Load up the first example and set the buffer to 10,000 and compare it to the second example with a buffer of 10,000.

     

     

    I completely get how in other languages you can get gains by using pointers and such and avoiding copies for large data structures by operating directly in the buffer's memory space, but in LabVIEW there's just no way to do that and you'll always need to copy an element into a scope local to each reader.

    Agreed. Shame though :P

     

     

     

    I mean if the index logic is separated from the buffer access logic, does it really matter if only one task can actually access the buffer at a time when it comes time to actually read/write from the buffer? I completely understand that yes, you may get a few hundered nanoseconds out by having one task modify an element while another task reads a different element, but if any real implementation that uses this fast access consumes a hundred times more time, I'd argue there's no real point. What are the relative gains that can be had here? How do those gains scale with the frequency of hitting that buffer, which depend on the number of readers and how fast the cycle time of each reader is?

    You can try this yourself. Set all the read and write polymorphic instances to non-reentrant.(the write/read double, write read index etc inside the read and write VIs)

    You've hit an important point here though. Scalability. This scales really, really well. Make the number of readers, say, 10 and there is a marginal increase in processing time. For queues, it is a fairly linear increase.

    All in, fun stuff. Please don't mind my poking about, the theory of this discussion fascinates me. I doubt I can offer any real insight as far as implementtion goes which any of you haven't already considered.

    Oh. I don't know. Now that you have the buffer that doesn't need DVRs, globals, LV2 globals or singletons, it can be put into a class. Basically the incrementing counter (the feedback node in the reads) just needs to be in your private data cluster. Then you would be in a good position to figuring out how to manage the reader registration (The ID parameter) That sort of stuff is only just on the edge of my radar ATM. But nothing to stop you coming up with the class hierarchy for an API since you don't have to worry about locking/contention at that point.

  6. unless you can limit the possible accesses significantly in terms of who can write or read to a single element. If that can be done then you could get away with referenced access to the array elements

     

    This is exactly what is being achieved with the LV MM functions although we cannot get reference access, only a "copy from" since to get back into labview we need "by value."

  7. Well lets forget about the polymorphic data storage aspect for a moment then. That is IMHO a completely separate issue.

    Indeed it is. But it is what makes it usable.

     

    What you therefore want is a managing infrastructure for the indices and all for both reader and writer but in a way that they are not globally locked but only locally protected. What I didn't like in your example (the first I think, didn't look at the others if there are) was (besides the use of globals of course :D ) that the implementation of the reader index does absolutely not scale. There is no way to add an additional reader without complete modification of just about anything in there. So I think the first approach would be to have the reader index as an array, and hence why I came up with the reader registration. Now you are right that globals have some sort of protection but only immediate access, you can not have a protected read/modify/store without additional external synchronization means.

     

    The question is, do we need protected read/modify/store at all? I think not, as there is only really one writer for every variable and the variables being integers have in itself atomic read access guaranteed on all modern platforms. So what about making the reader index an array? I think that should work. And if you separate the index management from the actual buffer data storage somehow I think that the delays caused by a non reentrant call to the index manager entity (be it an FGV or a LVOOP private data) should not cost to much performance.

    If protected read/modify/store would be required, maybe the inplace structure node might help. It seems to do some sort of locking that makes sure that the data can not be in an intermediate state. If that fails the only solution I would see in order to avoid explicit semaphores or mutexes would be the use of some external code to access some cmpexch() like function.

     

    Bingo! No. We don't need protected read/modify/store IF we are using the LV memory manager functions. The reason being is that we can pick and chose individual elements of arrays to update. you cannot do this in labview without reading the whole array, modifying and then writing the whole array.

     

    This is why I'm not worried about registration. Originally I had an array for the indexes in the global variable. But the obvious race conditions (read/modify/write) caused me to revert to single indexes fixed to the number of readers I was testing so I could get on and benchmark. It is/was an interim solution as, at the time, it wasn't clear whether it was worth spending the time working out a registration scheme if the performance was atrocious.

    Now I'm not using a global. This is a no-brainer and doesn't require additional logic, just a pointer to an array of indexes. The writer only needs to know how many are registered and then can do an array min on all of them. I'm thinking of 2 arrays, one for the readers' counts and one for the readers' R indexes as the two calls are probably more efficient than one call to a 2d flat array with all the gubbins required to extract the dimensions.(That's just a gut feeling). This part could, of course, also be done in native labview. It's the readers that are the problem.......

    Because the writer only reads the readers indexes and the readers only write to them AND the only important value is the lowest; if it is possible to update a single location in the array without affecting others, then no locking is required at all (memory barriers are sufficient) and no read/modify/write issues arise. Moveblock enables this but any native LabVIEW array manipulation will not (AFAIK).

     

     

    Incidentally I have been struggling with this type of thing just now for some external code (LuaVIEW for those wanting to know) where I needed to be able to update internal flags without running into a potential race if some other code tries to update another flag in the same data structure. The solution turned out to be the cmpexch() or similar function which is a function that atomically compares the state of a value with an expected value and only updates the value with the new value if that compare is positive.

     

    So to set a bit in a word I could then do something like:

    long atomic_fetch_and_or(long *value, long mask)

    {

    long old;

    do

    {

      old = *value;

    }

    while (!cmpexch(value, old, old | mask));

    return old;

    }

    In C this is fairly simple since the cmpexch() (or other names) is a standard OS function nowadays (or usually a compiler intrinsic) but there are exceptions in LabVIEW land such as the older Pharlap based RT targets and also the VxWorks based ones it seems. At least I couldn't find a reliable cmpexch() or similar function so far in the VxWorks headers and Pharlap ETS before LabVIEW 8.5 or so did not have a kernel32.InterlockedCompareExchange

     

    This are so called lockfree mechanismes although personally I find that a little misleading since it is actually still locking in the cmpexch() as that normally implements a complete bus lock for the duration of the assembly sequence, to prevent state corruption even when other CPU cores might want to access the same address at that moment. There are variations possible on the type of lock held such as only for write operations or only on read, but they make the whole story even more complex, are rather unportable between different architectures because of differences in their semantic so that I don't think it makes much sense to bother about them for more general purpose use.

     

    cmpexch (CAS) is an optimised CPU instruction so the processor needs to support it (if it's INTEL, it's a given, PPC - not so sure). I was hoping this was what SwapBlock used as it could potentially be more efficient than moveblock. But the only reference I can find to it was on a German forum and you provided an answer :D. I can't see any other reason for SwapBlock other than to wrap cmpexch The only people that can answer that question is NI, really. For my purposes, I don't really need the compare aspect, but an in-place atomic exchange would be useful for the indexes.

  8. Personally I think DVRs are probably the way to go here. They have reference semantics so you won't have to copy all the data each time eventhough I 'm pretty sure the DVR overhead will be negative in case of scalar buffer elements but positive for more complex datatypes.

     

    The only external code solution I could think of would be the use of the ILVDataInterface but I'm now pretty sure that is exactly what the DVR actually is internally about and I do not see any benefit in moving that into external code as in both cases you would have to use polymorphic VIs to support multiple datatypes.

     

    About the writer needing to know the index of the readers this would seem most easily solved by having all readers subscribe themselves to the buffer and getting back an refnum (index) into an array of indices where the buffer stores the reader index. Each time the reader wants to retrieve its data it then has to hand its refnum and the buffer retrieves the data for the reader and updates the according index. Personally I would use a functional global for the buffer management including the reader indices, but doing it with LVOOP would allow easy instantiation so you can have multiple circular buffers in an application without having to resort to making the FGV itself indexable too.

    Registering etc isn't a problem. The problem is the access to the indexes and the buffer without locking (mutexes etc) which is where the pattern derives its performance gains. My first choice was a class since the buffer and/or the reader/writer indexes can be held in each instance. However, the locking overhead around the private cluster coupled with atomicity of the private data clster means the performance degrades significantly (we are talking ms rather than us). You need a way of having indexes and buffer controls being able to be accessed independently (e.g. you cannot have them all in one cluster) and break access restrictions so that a reader can simultaneously read data when a writer is writing without waiting (which you cannot do if you wrap anything in a non-reentrant VI).

    You end up in a kind of no-mans land where you need a global storage (i.e. you need something like a DVR or LV2 global) with no locking mechanisms (requires reentrant like access behaviour). The closest native labview object that fulfills that is the global variable. If someone else has an alternative. I'm all ears.

  9. This will be running on a pxi chassis which I think runs Pharlap or VxWorks, but the previous code was running on something that was modern at the time I was born, so I have no idea what the byte alignment on that was. Then data was then read in with a more recently written program, using what looks to be c++/MFC. This application doesn't do anything fancy, just reads the records into a structure: lRet = hDataFile->Read((char *)&Dfr+512, Dfr.Hdr.RecordLen-512);

     

    I suppose i'll have to do some testing using the trial and error method to see what I get.

     

    Edit: I did some digging. I remembered the old application transferred data, and all logging was done on the windows side. This means it had 1 byte alignment, but now that's probably out the window (no pun). Does this imply I will have to write element-by-element and cannot just flatten a cluster?

     

    PXI comes in windows or RT flavours. RT (aka Pharlap ETS) is a cut-down windows kernel so for your purpose it makes no difference. VxWorks is for Power PC platforms so you will only see that in [some] CRIOs or Fieldpoint units. I wouldn't worry about it. I was just pointing out that byte alignment padding is not the same across platforms and assuming clusters are byte aligned can yield unexpected results on them.

  10. Was the memory manager the first place you looked? There are definitely ways to do this in native LabVIEW code...

    Nope. The first release was using a global variable and an array index. That was after looking at classes, DVRs and LV2 globals,  Most work OK until you get to the writer needing to know the index of all the readers  Do you have something specific in mind?

  11. Now, some more comments about the pattern itself... this post wanders from one topic to the next haphazardly. I'm regurgitating my notes more than trying to polish up a discussion of this pattern. ALL OF WHAT I SAY BELOW IS SUBJECT TO FUTURE CORRECTION BY MYSELF OR SOMEONE ELSE WHO SEES SOMETHING I MISSED.

     

    Good get-out clause :D

     

    To be generally usable within LV, the data structure is going to need some strengthening and probably some language tweaks (aka changes to labview.exe and lvrt.dll). The items in the buffer in Shaun's implementation are doubles. But this pattern is intended to be used for larger object-like data structures that are used by reference in multiple threads of operation. The objects are preallocated into the ring, the writer sets their fields, and then the objects are used simultaneously by both (all) readers.

    Yes. Variants I think are the prime candidate for LV.

     

    The system of the disruptor works in these other languages *by contract* not by language/environment enforcement. In other words, those other languages have no protections against a write to a data structure happening while two readers are both reading it. Allowing a truly "use at your own risk" structure in LV is antithetical to the design of LV. We do not have any sort of "typed raw memory address" data type in LV specifically because that isn't the design environment of LabVIEW. R&D has repeatedly refused to implement anything like that even under the argument that "advanced users could use it to write high-performance code".

    I don't think we need to "allow" it at all. They have certain implementation aspects that address atomicity, but we don't suffer from it because of LabVIEW. So we can ignore those aspects as long as it doesn't affect performance. What we do need is to be able to read a single or multiple elements from an array without copying the entire array, hence the move to LV memory manager functions.

     

    LabVIEW tries to be a language that is reliable in the hands of any of its users, and such raw pointer constructs aren't provably reliable. But although LabVIEW does not have any sort of "shared read-only large data structure", it does seem like there should be a way to introduce a DVR-like reference that has a modal switch on it where it is "read only for now" and then a writer comes along and gets an error if there is any reader currently reading and otherwise flips the mode to "writing now" during which any readers or additional writers would error out.

    Again. I don't think a new primitive is needed although a DVR would have been the first choice had it not come with all the baggage. It is as close to pointer referencing as we get and that is what we need. A global variable has most/all the features we need which you are describing here, without all the external signalling. It just comes with the baggage of copying entire arrays. Works great for <500 elements, but too much copy overhead for larger buffers. Don't forget, that a writer can't write whilst a reader is reading by the design (the counters), not by the storage locking mechanism. I think we can do everything with a bit of lateral thinking with what we have rather than require specialised semantics.

     

    These would not be locks, just atomic instruction bookkeeping. I stressed that because if a disruptor were properly implemented (i.e. the timings were set up correctly), such bookkeeping would -- in theory -- be limited to an additional Boolean test-and-set instruction on each write and each read. Minimal overhead. But it would be sufficient to trap an improperly implemented disruptor and produce a runtime error without seg-faulting the execution environment.

    See my previous paragraph. Unnecessary I think.

     

     

    Assume that we add the bookkeeping checks above (which *I think* could be added to the cluster that Shaun has created), there's still a problem of data copies of the complex data structures in the ring. If we work on changing the double data type out for a string data type (or any of the non-flat types, i.e., anything other than Boolean or the numerics or clusters of the same), we'd need a way to teach LV that the output of that DLL call is not stompable so that the implaceness analysis goes ahead and makes a copy if it is used in a stomper operation. I'm not sure what that trick would be, or if there is a way to do it. Suspect it is likely possible to do with a Call Library Node if we lie and say that the method call does not modify the value when in fact it actually does. Would have to play with that a lot to be confident of it working... might not be possible without revving LV.

    I think the idea that the buffer "contains" the data object is perhaps misleading. I am expecting the buffer to contain pointers to the objects and the reader just de-references (if we can get away without a copy, even better). So you create your variants then stick the addresses in the buffer. The writer can change the variants by the normal labview methods. The reader can read via the normal LV methods (after de-referencing). The counters make sure the writer doesn't update a variant that hasn't been read yet. All is happy in R&D because nothing ever gets stomped on to require management

     

     

     

    Anyway, both of those modifications are for the future. For now, we need to work with the benchmarks to see if there really is any advantage in LV for this pattern. So far, I'm not seeing it when compared against the 1QUEUE benchmark. Seems to be pretty much a wash, especially when the full implementation of disruptor will need a few more flag checks to be fully robust.

     

    Now, having said all of that, the ring buffer concept when we talk about network transmission is interesting for the NAK replay abilities. In LV, that would still require copying data out of the buffer for all non-flat data types, but it might be a really nice way to handle network traffic on a noisy line. All that requires is a LV array in a shift register, with none of the rest of the disruptor fanciness. That's definitely worth exploring if you're writing that kind of network-retry code.

     

    Done with the data dump. :-)

     

    [EDIT] Oh, and this comment from Trisha's comments is interesting:

     

     

    "The RingBuffer is not the secret sauce in the Disruptor's performance, in fact in the current version of the Disruptor you don't need it at all."

     

    Without the ring buffer? The only way I'm understanding what is going on here is with that buffer... not sure what Disruptor is without the buffer.

     

    No idea what she means there. It looks to me to be the very heart of the disruptor pattern unless she is using the name to reference the resource access and boundary contracts. But they wouldn't work well with anything else but a circular buffer as as far as I can tell. I see the elegance in the pattern in the way the writer and readers are able to circumvent lock contention (on a circular buffer) with simple counters and memory barriers. The usual method is to dollop large quantities of mutexes around.

     

     

  12. In case it wasn't clear this morning, I'm pretty sure that the proper compare for this proposed algorithm is against the single reader that executes both operations in parallel. I'm attaching my rewritten benchmark VIs here. Now, the benchmarks I'm attaching are testing have some overly large values for the time of the operations -- 50ms and 5ms respectively -- which means that the data transmission times are completely subsumed by the data operation times... scale these down to much shorter (but similarly skewed) values to do the benchmarking. I have the long delays in there because those are the values I finished on when I was using it to validate the benchmarks themselves... probably want to make these into parameters in the long run so we can get a feel for how fast the data operations need to be before this disruptor pattern has any value.

     

    Details:

    Put the "Average Array.vi" into the "TestUtils" directory.

     

     

    CB Test BUFFER.vi is the test of Shaun's implementation of disruptor.

    CB Test 1QUEUE.vi is what I think is a correct comparison of disruptor.

     

    In case I'm wrong on some front, I've included "CB Test QUEUEPAIR.vi" which is the double-queue implementation.

     

    I've moved stuff to For Loops so that LV preallocates the arrays so that ceases to be an issue. I cleaned up the sequencing of timer calls so that there's minimal ambiguity about which timer goes off first. I say "minimal" because the final timing read after each of the For Loops is totally unpredictable -- sometimes LV waits until all of the For Loops have finished before any of those timer calls get executed, and sometimes they execute in reverse order of the order the loops finished. I consider the internal timings to be junk data -- I'm not sure why I left them in place. The outer timer check around the entire write and read operation (labeled as "Total Time (sec)" on the front panel) is the interesting value that we are trying to minimize.

     

    Hmm. I still don't think you've quite grasped it, but I think we are nearly there :) . You are "averaging" a total time. That's not what we want to do as it doesn't tell us anything meaningful. With the default values of your benchmarks; we see some straight lines until the buffer is full in both. But what are the values you see periodically in the buffer @ about 1us that do not appear in the queue example? What is going on there? Data being processed? (you betcha)

     

    To demonstrate with your new benchmark, set the buffer size to 2 (This scenario is more exemplified for, say, an FPGA or RT where you don't have GB of memory for your queue-fixed sized array ring a bell?). You will notice there is not much difference in your "average times" and the queue write gets into its stall-state much more quickly but everything else is pretty much the same. But look at the graphs and the median of the buffer. They are definitely not equivalent even though the "average times" say they are. .You have not been kind to queues in this example since the write will block until the largest process time has executed (once the buffer is full) before moving to the next. The buffer doesn't suffer as much from this limitation and my benchmark was so that queues didn't suffer from it either.

     

    You asked me previously what time was I trying to optimise (which I erringly failed to answer-apologies). Well. The time I'm trying to get below is 1usec for the execution of a VI - the buffer access time. I am, at the moment, comparing the execution time of the VIs I've written with the queue primitives (which are compiled code). The next thing I am looking at is the "jitter" and then trying to identify any blocking modes which may indicate resource contention locking. This was the reason for moving from the global to the pointers since a read of the global array locks the writer (albeit transparently) and forces a copy of the entire array. Once I can figure out variants (IF I can figure out variants) then I'll start looking at 5-10 readers with one writer (that's going to be a tedious weekend  ;) )

  13. I'm almost 100% sure it is not documented. And from the looks of it it won't really help you here. The function linked to by the xnode is the only useful exported function in there. If the xnode doesn't know how to deal with variants to make the xnode interface work with that library function, the library function itself most likely doesn't know either.

     

    But can you explain what you try to do? Do you really need the runtime variant type feature of a Variant, or do you just want adaptable code that is fine to get the right datatype at edit time? If the second is true, some polymorphic VIs and possibly the ILVDataInterface in combination with Adapt To Type as Call Library Node parameter type might be enough.

     

    Just be aware that ILVDataInterface is only available since LabVIEW 2009.

     

    It was a long shot and I didn't really have any expectation it would help. But it was worth asking the question.

     

    As to what I want to do. You will need to download the code in the Queues vs Circular buffer thread. It's fixed in the demo using doubles for the buffer. I need to be able to allow wiring of any data type so I thought, naively, just adapt to type to a variant. It seems to work great as long as you don't de-allocate the pointer :D. Maybe I don't need to deallocate,it but it "feels" wrong and probably leaks memory like a sieve as well as a high chance that a gremlin is sitting there just waiting to raise his GPF flag..

  14. Sorry... I've been on vacation and out of wi-fi range (enjoyably!).

    I thought holidays were for not doing what you do at work :P I'll still be at this when you get back, so just enjoy your holiday.

     

    I've got some very weird results at the moment... trying to figure out what's up. On any Mac, the graphs are flatlined. Is that just an artifact of the low-resolution timer?

    Most definately. Yes. The timer uses the windows hi-res timer (on windows, of course) but falls back to the ms timer on other platforms.

    I'm also trying to figure out which time we are trying to minimize... I think we want to minimize the total execution time of two readers, one with a short operation and one with a long operation. That's not quite the same as comparing against the two-queue solution. Are we supposed to be comparing against a single reader that does both operations in parallel or a single reader that does both operations serially?
    The benchmarks are just to ascertain the execution time of the read and write VIs themselves and what jitter there is from execution to execution. The theory is that because there are only memory barriers and no synchronisation locking between a read and a write, the "spread" should be much more predictable since, with a sizable buffer, access contention is virtually eliminated. In that respect, the queue benchmark isn't really a "use case" comparison, just an examination of the execution time of the primitives vs the dll calls with logic. Don't forget also, that the buffer has inherent feedback in that the writer can slow down if the readers can't keep up. If we were being pedantic, we would also enforce a fixed size buffer so that the write "stalls" when one or more readers can't keep up. I don't think that would be useful however.
     

    Finally, if you put the buffer solution in a loop, it deadlocks. Trying to figure out how to get the state out of the read function so that it becomes part of the input -- I think the right way is to structure this as a com-channel where you create reader refnums from the  com-channel refnum, readers that are invalidated when the main goes stale.

    I'm not sure what you mean here by "buffer solution in a loop". Can you elaborate?
     

     There's also a memory leak if you abort or run to complete without calling the uninit VI. This is caused by not having any way to throw away the buffers that are allocated in Init when Abort/auto cleanup runs. The call library node has callbacks to handle these, but we would need to create an actual DLL to handle that work.

    Indeed. I'm trying to keep away from external code......at least for now. There is still a lot to do to make it an API. I'm still investigating its behaviour ATM so this thread is really just documenting that journey. If you remember the SQLite API for labview, that's how that started. So right now we have some of the building blocks, but a lot needs to be added/removed/modified before it's ready for "prime-time". The issue is going to be. How do we make it robust without enforcing access synchronisation.
  15. Well, that made me think! It might be not the LvVariant that is refcounted but the data inside. Take a look at ILVDataInterface.h. I think all the diagram data is probably wrapped in that and refcounted and then the LvVariant is a thin wrapper around that. To manage polymorphic data. The ILVData interfaces in there inherit from IUnknown, most likely the same as the COM IUnknown interface, eventhough NI seems to have gone the path of creating a nidl tool that replaces the midl compiler from MS in order to make it work for non MS-platforms.

     

    Quite an undertaking but I would certainly start with the source code from Wine for that, who made a widl tool! While Wine is LGPL, as long as it is only for inhouse use, and I doubt NI ever planned to release that toolchain, this would be basically still be fine.

     

    Now you are showing off :D

     

    If it's really that hard (and undocumented), I'll do something else.

  16. I don't think so. Handles do not really maintain a ref count in itself. And I doubt a LvVariant is a real handle although it seems to be a pointer to a pointer (or more precisely a pointer to a C++ class).

     

    I would suspect the LvVariant to implement some COM like refcounting but would not know how to get the refcount properly increased and decreased.

     

    I don't really see why variants need a ref-count since you can disassemble a variant as bytes and reconstitute which would mess up that sort of thing.

     

    I read (somewhere) that for handles you have to query the "Master" handle table to get hold of a handle to a particular variable so it is a two step process. So I was thinking perhaps the issue is that I'm actually operating on a master table rather than the real handle.  I was kind-a hoping it would just be just a miss-setting in one or more of the CLFNs. Since you haven't proffered a solution or things to try with pointers, I guess it's not trivial and I'll just have to jump down that rabbit hole to see what Alice is up to..

  17. That is most likely not a real fix but just an avoidance of a symptome. It looks like LabVIEW is actually maintining some form of reference count on Variants. Copying the pointer alone is likely not enough to keep the variant alive, but somehow the reference count has to be incremented too (and properly decremented afterwards again, to avoid lingering Variants.

     

    Maybe that's the difference between working with pointers and working with handles? (DSHandToHand etc). Could be that variants are relocatable.

×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.