Jump to content

Aristos Queue

Members
  • Posts

    3,183
  • Joined

  • Last visited

  • Days Won

    202

Everything posted by Aristos Queue

  1. My original comment on this topic: So on the write, you can use the swap because the source is stompable. On the read, you would need to make a copy. It's a bool. It controls something. I don't know what... didn't dig into it. It's private data of a class, and anything that doesn't go through a class' API will just break when someone moves the private data of the class around or changes the data consistency rules for that data. I figured I'd list off the fields I thought you could hack for the moment, but going into all the details of the class is pointless -- the operations that you can manipulate are given in G primitives already.
  2. The processes are parallel -- the computation done on each value is done in parallel with the computation done on the other value. The only reason you can make the argument that they aren't parallel is because you have contrived that one operates on the odd values and the other operates on the even values. If you have a situation like that, just stick the odd values in one queue and the even values in another queue at the point of Enqueue. Now, I realize you can make the system "both loops execute on 2/3rs of the items, with some overlap" or any other fractional division... as you move that closer to 100%, the balance moves toward the copy being the overhead. My argument has been -- and remains -- that for any real system that is really acting on every value, the single queue wins. If you have two processes that have dissimilar execution, then just arrange the first one first in a cascade, like this (which does run at 500 ms): AQ1_CascadeQueue.vi
  3. Yes. Doh! That's why you do a top-swap instead of a move block. There's one allocated inside the variant in the first place and you swap the [pointers/top-level data structure which may our may not contain pointers depending upon the data type under discussion] so that the buffer still contains one.
  4. Completely wrong. I'm going to state this about five different ways because I'm not sure which metaphor will be most helpful for you here. I'm going to keep offering variations on this theme until I can help you see why because understanding this matters massively to your ability to architect within LabVIEW and because you tend to teach others. Your statement would mean that timestamps are equivalent to an FGV because you can only get to the cluster of individual fields (like "hours") by using a particular function. In fact, it would mean that all data types are equivalent to an FGV because you can only use certain functions to access their data -- right down to only certain functions can access the bits of a numeric. A class does not define a data location. It defines the rules for data *consistency* -- in exactly the same way that the functions that manipulate Path keep the internal data structure consistent. If it helps you better understand why these have nothing in common, go make every method that has a "obj in" and "obj out" of your classes Reentrant. In 99.99% of cases, this will not break the class functionality (the 0.01% are in classes that contain reference types or methods with side-effects). But that same reentrancy change completely destroys the FGV. One is defining data storage. The other defines data behavior. To put it another way, the class and the FGV have *nothing* in common. One is a data type. The other is a storage mechanism. The FGV might store a class or a numeric or any other data type. A bit of data on a wire is local to the executing VI. A bit of data stored in an FGV is global and shared. The object on a wire is local execution and is FULLY ACCESSIBLE by the local calling environment via the functions provided by the class. In the 99.99% case, that means they do not share any state with any other call to that same function. There is no lock on a class' private data any more than there is a lock on the bits inside a numeric control or the fields of a cluster. A class in LabVIEW is NOT a reference data type. There is no single object that every wire points back to. There is just a blob of data that when the wire forks becomes two distinct and independent blobs of data. The methods of the class do nothing more than define the rules for how that blob mutates as it passes through those functions. Users can abuse classes with any number of embedded reference types or methods with side-effects that play havoc with this basic definition, but that isn't particularly common (it may be common these days to wrap references in classes, but it isn't common for most classes to be wrapping references -- most classes are indeed by value types, and most code that I see from customers these days would benefit greatly in terms of code clarity if every single cluster and many of the lone elements (particularly strings) were replaced with a class with no loss or change of functionality). A timestamp is not equivalent to an FGV. A path is not equivalent to an FGV. A cluster is not equivalent to an FGV. In *exactly* the same way, a class is not equivalent to an FGV. An FGV is equivalent to a DVR. It is equivalent to a single-element queue. It is equatable to a notifier. None of these are equatable to an array. or a cluster. or a class. Did any of that make sense? I've stated it as many ways as I can think to state it. I think that's my point -- I have a hard time believing that "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.
  5. 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.
  6. 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. It's fairly common, but that's why forking the wire to two read-only operations generally suffices to cover the use cases. That's why I posted my revised benchmark of a single queue where the output is "forked" to two separate operations (in the benchmark, the two operations -- both Wait Ms calls -- don't actually use the values but they represent operations that need them and do not modify the values). LabVIEW allows two parallel operations to operate on the same by-value data element as long as both forks are read-only operations. That's how you get the pointer sharing in LabVIEW that is so commonly needed in other programming environments. 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. It's going to be unstable in LabVIEW for any non-flat data type without some serious re-education of the inplaceness algorithms to be aware of such unorthodox sharing. Strings, paths, waveforms, arrays, objects -- these are all going to run into problems if you try to avoid duplicating the underlying memory when putting it onto two separate processes.
  7. Variants? You'll lose any performance gains in type checking when you actually want to manipulate the data. If the final solution has anything to do with variants, I'll be very surprised.
  8. 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. 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. 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". 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. 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. 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. 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.
  9. 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. CB Test 1QUEUE.vi CB Test BUFFER.vi CB Test QUEUEPAIR.vi Average Array.vi
  10. Sorry... I've been on vacation and out of wi-fi range (enjoyably!). 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? 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? 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. 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.
  11. I've been rewriting the benchmark VIs to my own standards. An important change is making those While Loops into For Loops and adding some sequence structures because the timers are being delayed by panel updates. Also the queue and buffer benchmarks do not match. I'll probably post revised VIs late tonight.
  12. The questions being asked here are exactly the same questions asked at NI when DAQmx was designed. There were many debates over the trade offs of which way to slice functionality for max polymorphism (which has user advantage insofar as lots of measurements have a similar API) and what to make runtime vs compile-time errors. You see the result. If you're trying to write a *general* OO wrapper, I bet your wrapper will just trade one set of issues for another. But if you design an app-specific API to wrap DAQmx, I know you can put a much easier to use API into place... I know because I've seen users do it.
  13. It's July 4 week. They could easily all be on vacation.
  14. Shaun: Yes, the parallel queues make copies of the data. The only advantage I saw for the detonator was the lack of copies. So if I end up making copies, there's no advantage. Mje raises the point about the buffers. That's a reasonable point. I think that argument might flip the advantage back toward this being useful even in LV.
  15. > Which method did you use to create the ring buffer? I haven't gotten as far as actually creating any buffers -- I was just setting up the shell VIs for one writer and two readers. > Are you saying that merely using the index array primitive destroys the element? Using Index Array copies the element out of the array. That is *exactly* what made me pause: if at any point you make a copy of the data, you have lost the advantage of this system over the parallel queues. Since you *must* make a copy of the data in order to have two separate processes act on it at the same time, there's no way to get an advantage over the parallel queues mechanism. Their buffer system appears to take advantage of the fact that you can, in a thread-controlled by-reference environment, act on data in-place. That's impossible in a free-thread by-value environment. I stopped going any further than that until I clarified my understanding since if these thoughts are correct, there's no possible advantage in LabVIEW with this concept. If I've missed something, I'm happy to re-evaluate.
  16. This intrigued me, so I started to put the code together for this to see how it would work. After just a bit of setting up the VIs, I realized that I can't have two consumers walk the same buffer without making copies of the data for each consumer. That made me fairly certain that this ring buffer idea doesn't buy you anything in LabVIEW. In Java/C#/C++, the items in your queue-implemented-as-ring-buffer might be pointers/references, so having a single queue and having multiple readers traverse down it makes sense -- it saves you duplication. But in LV, each of those readers is going to have to copy data out of the buffer in order to leave the buffer in tact for the next reader. That means that you have zero advantage over just having multiple queues, one for each consumer that is going to consume the data. It could save you if you had a queue of data value references, but then you're going to be back to serializing your operations waiting on the DVR to be available. Am I missing some trick here?
  17. I posted something like this a few years ago... http://lavag.org/topic/5047-prototype-replacement-for-error-llb-second-attempt/ I'm not sure why the attachments are missing -- seems they've been deleted from LAVA. I've worked on this off and on for a few years. Never gotten enough impetus to publish it into LV -- something always takes priority. I've actually got a version that is pretty advanced that I've been polishing lately. Maybe I'll pull it together in the next couple months to show off.
  18. Haven't seen anything like this, and I've ramped lots of actors at the same time of the same and of various types. Post your question on the http://ni.com/actorframework community site... let's see if any of the other contributors have seen this. As a test, try adding a time delay in your code between calls to Launch Actor.vi. I *really* hope that does NOT fix it, because if it does fix it, you're really going to stress me out that there's some sort of deep race condition, probably in LV itself, and those are NEVER easy to track down, and we are WAY late in the 2013 release cycle.
  19. For the record, there's a truly fascinating design that I've seen implemented. 1. Make class Message Handler as a *by value* object. In other words, it has methods for "Handle Message X" and "Handle Message Y", and all of its data is within itself. 2. Put that class in a shift register of a message handling loop. Dequeue a message, figure out the type of the message, call the right method (how you figure out the message type -- strings, enums, dynamic dispatching, variants, finger in the wind -- doesn't matter). 3. Some messages tell the message handler to "change state". Let's say message X is such a message. In the "Handle Message X" method, instead of changing some internal variable, output an entirely new Message Handler object of a completely different Message Handler child class. That now goes around on the shift register. All future messages will dispatch to the new set of methods. Any relevant data from the old object must be copied/swapped into the new object. This eliminates the case structure in the message handling of "if I am this state, then do this action" entirely. Because we made the output terminal NOT be a dynamic dispatch output, you can implement this in the Actor Framework and switch out which Actor is in your loop while the actor is running. This is known in CS circles as the State Pattern.
  20. The dot language used by Graphviz comes most easily to mind. Next closest would be XAML, in my observation. Both of those are declarative languages, meant for describing layout and connection. dot can express cycles in graphs, but it lacks the positional information (as that is supposed to be inferred by automatic layout). XAML has the position and is richer in its syntax checking. Although scripting either of those would be easy enough, programming in them would be very hard. Graphviz separates declarations of nodes from declarations of connections. XAML would require something similar... you would have to declare wires separate from nodes or something weird like that because XAML expresses a tree natively, and any time two parallel paths came together, you have to introduce some sort of "that's me over there" notation. As for the procedural, dataflow and functional languages... any save format that allowed you to write G would look *nothing* like any of those programming languages. There are things we could do in other languages that we could *interpret* as a VI (for example, single assignment C), but to save all the positional/configuration information, you need more notation than any of the textuals generally have.
  21. By the way... the data dump I did earlier missed one point. Although I don't really like a single diagram for defining a class, a diagram for defining a class *hierarchy* is something I have a keen interest in. It would not look like an execution block diagram at all. The Symbio and G# UML tools make an excellent stab in that direction. I mention this because rereading the above, I realized that someone might think I don't want a more graphical way to define classes. This is something I *definitely* want. I just don't think that a VI is the way to do it, and I don't think separate diagrams per class are the right way to go. Just sayin'. :-)
  22. Those refnum types will all be 32-bit integers in VB.Net. Just declare variables of type "int". In the case of the modifiable refnums, use "ref int". In answer to your second question of how you get valid values for those as inputs, one of the other functions will be supplying those values. There should be some function on your LV DLL like "Open Port" or "Create XYZ Reference" that only has those refnums as outputs. You'll pass the result to the other functions.
  23. See: http://www.ni.com/white-paper/3574/en/ Section on "Can I overload VI names in classes?"
  24. The Actor Framework dispatches on message type first and then in the message handler evaluates the current state to decide what to do. This choice was forced by other aspects of the system, not something that was deliberately chosen. It has the advantage that adding/removing messages is very easy. Adding/removing states is thus commensurately harder (you have to visit every handler). You might want to decide based on which you are more likely to add/remove -- states or messages. Now, my suspicion is that *most* of your messages behave the same in every state, and you'll have less duplicated code if you make the decision based on message first and then on state. Even when the code isn't exactly the same, deciding on message first gives you a way to put common code in for a message either before or after you do the state-specific work. Of course, the counterargument also holds that a given state might do work before/after every message... so the only real decider here would be the duplicated code if most messages do the same thing regardless of state. Thus my lean would be toward message first, but I don't see anything fundamentally wrong with the other.
×
×
  • Create New...

Important Information

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