Blog Closed

This blog has moved to Github. This page will not be updated and is not open for comments. Please go to the new site for updated content.
Showing posts with label IO. Show all posts
Showing posts with label IO. Show all posts

Monday, June 8, 2009

IO Speedup, More Dramatic Still

Two days ago I talked about how the speedups in the io_rewiring branch were approaching 4x by some benchmarks. I posted an email to the list and got back a few replies which seemed to show the speedups were not so impressive as that. Specifically, partcl didn't appear to appreciate the work we've been doing, even if Rakudo did. No big deal, any speedups for any users would have been a win in my book, even if not all compilers on Parrot benefited from it. Some people winning is better then nobody winning. Luckily this isn't the end of the story.

When I logged on to IRC this morning, Infinoid had a commit comming through that would make everything better still. Here are some very impressive results that Infinoid has measured with his patch in place:

Infinoid> before: make coretest 84.97s user 61.91s system 32% cpu 7:35.52 total
Infinoid> after : make coretest 75.22s user 43.93s system 67% cpu 2:57.62 total
Whiteknight> so that's a factor-of-2 speedup on top of the 4x speedup that some
benchmarks are showing?
Whiteknight> Infinoid++ # Holy shit

That's right. In the branch, on top of the previous speedups we had, Infinoid has more then halved the running time of make coretest. This is a total of 8x speedup on the one benchmark that we had been using. However, this isn't even the most dramatic result. pmichaud had a doozie for us that I wouldn't have believed if I hadn't seen it (and I still might not believe it):

Infinoid> here's stats for pmichaud's benchmark:
Infinoid> trunk : ./parrot x.pir 12.37s user 1.67s system 56% cpu 24.747 total
Infinoid> branch: ./parrot x.pir 0.42s user 0.06s system 64% cpu 0.741 total

You read this correctly, at least one benchmark is running 50 times faster in the branch now. As they say on the weight-loss commercials, "results not typical", but impressive nonetheless. More results:

Infinoid> trunk : make coretest 63.07s user 36.61s system 26% cpu 6:15.27 total
Infinoid> branch: make coretest 61.79s user 31.34s system 63% cpu 2:26.49 total

So what exactly was the culprit, the change that Infinoid was able to make that sped Parrot up by more then double? The problem, in a nutshell, was fsync. A number of IO API calls were using Parrot_io_flush, which internally would push the data to the OS and call fsync to ensure that the data was completely written to disk. This is unnecessary, and fsync was a huge slowdown. So what Infinoid did—brilliant in it's simplicity—was replace calls to Parrot_io_flush with calls to Parrot_io_flush_buffer, which would still write data out to the OS, but wouldn't call fsync anymore.

So what exactly is fsync used for anyway?

szbalint> fsync only makes sense after accumulating a largish chunk of data anyways
Infinoid> fsync only makes sense for databases and mail servers ensuring atomicity

...and neither of these things are really part of the guarantee that Parrot's IO system makes to it's users. Parrot does not specify that it's basic IO will be atomic in any way, nor does it specify that we will force a disk write after so much data has been pushed through. We certainly could add an opt-in mode to guarantee that (especially if we can get some of the buffering code factored out like we are planning), but it isn't a requirement for all cases right now.

So that's my little note this morning about our branch. Assuming testing keeps coming back positive, I'd like to get it merged in to trunk tonight so we can get started on the next round sometime after #parrotsketch tomorrow.

Saturday, June 6, 2009

IO Pudding, Contains Proof

I've been spending a lot of my time working on IO-related improvements to Parrot. I'm still working hard at the major refactors branch that I started with Infinoid, so I haven't taken any time to benchmark anything yet, and working through some of the changes that Allison suggested. However, Infinoid did run a quick test this evening:

infinoid> oh, wow
infinoid> I hacked examples/streams/ParrotIO.pir to read one of the biggest files in the tree I
could find (t/library/md5_4.pir) instead of the default (itself)
whiteknight> ...and?
infinoid> infinoid@chirp test % time ./parrot examples/streams/ParrotIO.pir >/dev/null
infinoid> ./parrot examples/streams/ParrotIO.pir > /dev/null 0.79s user 0.04s system 94% cpu 0.883 total
infinoid> infinoid@chirp io_rewiring % time ./parrot examples/streams/ParrotIO.pir >/dev/null
infinoid> ./parrot examples/streams/ParrotIO.pir > /dev/null 0.21s user 0.03s system 98% cpu 0.245 total
infinoid> I couldn't find a better benchmark (yet), but that looks promising

Don't want to start counting my chickens quite yet, but a 4x speedup is quite a nice early result. I have some cleanup to do before I call this branch complete and ready to merge into trunk. I doubt we will get any better then 4x speedups before then, without some more-substantive changes then what I have on my tasklist.

The majority of the test failures right now are packfile test failures, and I suspect it's because we added a new core PMC type and haven't properly updated the list yet. Infinoid said he's going to dig into that problem tonight, so maybe when I wake up tomorrow we will be passing all tests. Wouldn't that be a nice treat for a Sunday morning?

Update: Not more then 10 minutes after publishing this blogpost, Infinoid had the issue resolved and now all tests are passing. It really is an honor to be working on a project like Parrot with so many wonderful developers!

Wednesday, June 3, 2009

Re-Rethinking IO, Again

Had a good talk with Allison last night about my ongoing IO work, and got a lot of great ideas. It's good to have people in the project that are so great for bouncing ideas off of. We decided a few things:
  1. The way I was using "roles" and the does VTABLE was a pretty egregious abuse, and very misleading. It did identify capabilities, but that wasn't the right way to do it.
  2. If we're going to support polymorphism, we're going to do it right. Unfortunately, the right way to do it is through methods, which is what I was trying to avoid in the first place.
  3. So, for the common case of built-in PMCs and direct subclasses, we can use direct ATTR accesses to do IO quickly. For other types, we default to calling the appropriate method. This does the common case quick, and the fancy-schmance case less quickly.
So, with all this in mind I am going to revert several of the changes I've made to some files, and start redoing things in the "right way". It's never too late to get things right though, and I definitely don't mind the extra work. Here's what this all means:

  1. The common case, of builtin PMCs (FileHandle, Pipe, PipeHandle, Socket, StringHandle) will be handled quickly because Parrot's core has an intimate knowledge of the internals of these PMC types. Also, subclassing these types should Just Work, assuming we can abstract things properly so all ATTRs of these types are subclassable (INTVAL, FLOATVAL, STRING * or PMC *). Notice that the C-based PMCs converted with the Pmc2c.pl utility will have difficulty because the base type identifier will not match that of the builtin PMC types.
  2. The uncommon case, of using any arbitrary PMC type for IO, will be supported but less quickly. We will call named methods to implement various behaviors ("read" method to read, "open" method to open, etc).
  3. The user-visible interface won't be changing through any of these refactors, so we won't need a deprecation cycle. It's an in-place optimization.
So that's the changes in a nutshell, and I'm hoping that the end result is good and fast for most cases. I'm hoping to power out some changes this week before some of my other projects take over most of my free time. I am still planning to have these IO refactors land before the next release, so that I can get working hard on AIO in July.

Friday, May 29, 2009

IO Work Proceeds, with Questions

Earlier this week I kicked off work on a revamp of the Parrot IO system in the io_rewiring branch. The purposes of the branch are multifold:
  1. To improve speed, by decreasing reliance on PCCINVOKE calls to perform IO operations
  2. To improve flexibility, by realizing that IO-related PMCs do not all hold a common interface
  3. To make basic IO PMC types properly subclassible
The io_rewiring branch is being used primarily for tinkering right now. We still have a lot of questions open as to how we are going to fix everything. However, we have some general concepts that we are trying:
  1. IO API functions like Parrot_io_* are now called by the methods in IO PMCs, instead of the other way around. Most IO operations now, except those called from methods in PIR directly, do not use PCCINVOKE.
  2. IO PMCs subscribe to a number of roles that determine what operations they can and cannot be expected to do. A PMC that does "file" can 'seek' but cannot 'connect'. A PMC opened for writing only does "write", but does not does "read".
  3. The PMC inheritance hierarchy is getting a little bit more sane. Pipes are not going to be FileHandles with a special flag set anymore. Sockets are not subclassed FileHandles either. At the moment, all IO Objects are derived from a Handle type, but this may change. The imporant part is that they all does "IO".
  4. Buffering logic is (probably) being abstracted into a separate PMC type somewhere.
So what my goal is for this branch, in a nutshell, is to break a little bit of encapsulation for major gains in performance, flexibility, and subclassability. Instead of blindly calling a method to perform operations, we check the roles of the PMC and depending on what roles it implements we access attributes and methods as necessary. The reason I want to try this approach is simple: We have a series of PMCs, each with an internal implementation and an external-facing API. The problem is that none of the PMC types are going to have the same API: Not all PMCs implement 'seek', or 'connect' or 'fcntl' for example. So it doesn't make sense for all PMCs to blindly be implementing these methods, or having to have a million API methods to satisfy every need of every potential type. So the question comes down to this: Do we maintain a large standard API with more methods then any one PMC type needs, do we maintain a small standard API and shoehorn all IO PMCs into it, or do we not maintain any specific API, and assume all PMCs of a particular type share common internals that we can poke into directly?

And we're not really poking into the "internals", really. We're using named attributes which are easy to subclass from PIR and are being treated as public fields. We're also trying to use VTABLEs where appropriate. In fact, I have a major complaint about the massive overabundance of arithmetic-related VTABLEs as compared to the dearth of IO-related VTABLEs, but that's a different rant for a different day. If we make the rule that we only acces VTABLEs and named attributes, and if we properly document which of each the different IO PMC Roles require, I think this method should be fine.

Whether the work in this branch ever satisfies all my goals or not, and if so whether we get community approval to merge it into trunk, is up in the air. It certainly is fertile ground for exploration, however, and I'm taking the opportunity to explore in great detail.

Tuesday, May 26, 2009

Lots of IO work to do

It's no secret that I've been planning pretty aggressively to implement an asynchronous IO subsystem for Parrot. However, this is really just one part of a larger group of projects that the IO system needs.

Today in the weekly Parrotsketch meeting, a lot of IO-related issues were raised. Several people mentioned performance issues, which seems to be a hot button issue in Parrot-world and for good reason. The way the IO system API is currently implemented is as a thin wrapper layer around PCCINVOKE method calls for the FileHandle and related PMCs. This implementation works decently, and was necessary for a time to support performing IO operations on a wide array of hypothetical IO PMCs, but there is a huge overhead involved in calling all those PMC methods. In fact, it was this very issue that prompted the current round of PCC refactors. Instead of having IO PMCs related simply by sharing a common set of methods, it's probably much better for these PMCs to be related together by inheritance and to use a common set of C functions to access their common VTABLEs and attributes directly.

So Infinoid and I have been putting together a plan tonight and I'm pretty excited about it. Here are some of the main points:
  1. We're refactoring the PMC hiearchy. IO types will all derive from a common Handle type that will have a few common attributes, vtables, and methods.
  2. We're going to refactor FileHandle to subclass from Handle
  3. We're going to fix Socket so that it subclasses Handle, and not FileHandle
  4. We're going to use roles and the VTABLE_does to determine the capabilities of each type of IO PMC. Roles like "read", "write", "file", "socket" and "pipe" will determine capabilities. Then we can also have subroles, like "connection" and "connectionless" for different types of sockets, etc.
  5. Try to fix attributes, where possible, to be inheritable so we can subclass all these things from PIR reliably
  6. Change the IO API to use C functions instead of PCCINVOKE methods to perform operations. We sacrifice a little bit of encapsulation for big performance gains, but I think it's a worthwhile change.
After this, Infinoid is really looking to get an IO multiplexing "Select" PMC put together, and I'm looking at getting AIO in place. I was planning to do something like Select as part of the AIO work, but I'm not sure that my idea is the best for general purpose use. I'm still considering using an "IOListener" that will check IO events as part of the scheduler, but that could be the asynchronous equivalent of a manually-polled Select PMC. Maybe we need both, maybe we only need one. We'll see how it goes.

My goal is to get this cleanup work done within 1-2 weeks, and with a great coder like Infinoid in my team I think it's completely possible. After that, I have the ambitious goal of getting AIO implemented, or at least a working subset of it, by the 1.3 release (that I am the release manager for). Certainly by 1.5 Parrot is going to have a world-class IO system available, which is perfect timing because that's when a few of my other tasklist items become unblocked.

Sunday, May 24, 2009

Blocked Tasks and Starting AIO

I'm trying to take stock of my task list, and I have surprisingly few things available to do. The list is pretty large (and wasn't even complete, I added a new task this morning), but I am able to work on only a small handful of items right now.
  1. I am adding a new job to my task list: removing the deprecated stacks functions. I added in the deprecation notice in r39144, which means I can't work on this stuff until the 1.5 release in 3 months.
  2. The calling conventions work is all blocking, probably until 1.4 in two months. This is a pretty big issue and getting bigger, because I'm finding all sorts of new items that are related to this that I wasn't aware of previously. Most of the items on my list are blocked because of this.
The IO system is in better condition then I thought when I last looked at it, so it doesn't really require any cleanups, so check that item off my list. Also, yesterday I fixed the problem Coke reported about Objects, so that item is off too. So, what's left on my list that I can do between now and 1.4?
  1. Create the incremental GC
  2. The Asynchronous IO system
  3. Documentation (especially the book)
I do some idle writing on the book every now and again, but that hardly scratches my "need to code" itch. Plus, it's scheduled to go to the printer within the month, which leaves me with only two tasklist items to work on post-1.3.

So basically I have the incremental GC and the AIO systems to work on for now. Since I'm not ready yet to do the GC, I've started the AIO work this morning and have already started implementing some of the necessary PMC types. This branch isn't going to make any changes anywhere else in the code base, it will only add prototypes of new PMC types that can be added to trunk without breaking anything. Once these things are implemented, I will open new branches to start integrating them into Parrot. I'm leaving notes throughout to keep track of what the integration will require. There are also obviously still a few open questions to answer, and I'm hoping that these will get resolved as I start implementing things.

Friday, May 15, 2009

IO System: I Was Wrong

Today I opened a ticket about how the layering portion of the IO system specification needed to be implemented. It turns out I was wrong: Despite the fact that the PDD claimed a layering system would be used and the fact that there were some hints about it in the code, it is no longer the current plan that the IO system use a layering approach. So tonight after talking to Allison about it, I went in and ripped the confusing passages out of the PDD. I also went through and removed some unused code that wasn't being used.

So there really isn't anything standing in the way of a proper AIO implementation, except maybe a handful of enhancments to the threading and scheduling systems (I haven't dug into either too deeply yet, so I can't say for sure). That's good news, fewer things in the way means it's more likely we're going to get proper AIO by 2.0.

Monday, May 11, 2009

Reading Data from the Serial Port

At work I'm building a program that is, in part, a serial port console like HyperTerminal. It's not a pure drop-in replacement, we have some specific requirements that HyperTerminal didn't quite meet. However, a lot of the port-handling logic in my program could probably be repurposed for creating a more faithful drop-in replacement if the need arose.

The biggest problem for me at the beginning was deciding how to read data from the port. Obviously, we can't do the reading synchronously, in the case of a TTY-like program that just doesn't make any sense. I had two real options for asychronous operation:
  1. Create a worker thread that reads from the port, sleeps for a certain timeout, and then repeats.
  2. Use the DataReceived handler on the SerialPort object to automatically call a callback when incoming data is received.
People who read my discussion about AIO in Parrot will probably recognize that item #1 does not appeal to me at all. First there is the issue of synchronizing the thread and getting it to start and stop when I need it to do each. I also need to be able to pass settings to the reader, which would require cross-thread synchronization logic that could get really messy really quickly. Plus, if we have a thread timeout of 1second for example, there could be up to a one second delay between the time data is received and when it becomes visible to the user. We could easily set a timeout to zero seconds, but then we have a background thread endlessly looping over the port, usually reading nothing. Not exactly a performance killer, but still needlessly wasteful. Nothing we are doing is so time-critical that a 1second sleep timer for instance between reads would kill us, but it's still a clunky-feeling mechanism that will make the interface look amateur.

On top of all those reasons, as if I need another one, I feel like using an explicitly-managed thread here is a particularly inelegant solution. In short, I decided to use the DataReceived event handler to do my port reading (when I put my port into asynchronous read mode, that is).

That question out of the way, I needed to decide how to do input buffering. Do I buffer by line (SerialPort.ReadLine), "buffer" by character (SerialPort.ReadChar), or do I not buffer (SerialPort.ReadExisting)? On the one hand I need to support both buffered and non-buffered input. On the other hand I don't want to be stacking up DataReceived events with Timeout events endlessly, because that could create a stability problem.

My design goes as follows: I use the DataReceived event handler to signal me when data is ready. From within the handler I use SerialPort.ReadExisting to read the incoming data into a buffer, and from there slice and dice the input into the forms needed by modules higher up in my program. I admit that this may not be the most attractive or elegant code I have ever written, but it has demonstrated itself to be very performant and robust solution for my needs:
private string readBuffer;
private SerialPort port;
private bool unBuffNeedNewline = false;

private void InitPort()
{
this.port = new SerialPort();
this.port.DataReceived += new SerialDataReceivedHandler(DataReceiver_Handler);
}

private void DataReceiver_Handler(object sender, SerialDataReceivedEventArgs e)
{
if(this.bufferMode == ConnectionHelpers.ReadBuffering.LineBuffered)
this.DataReceiverLineBuffered();
else
this.DataReceiverCharBuffered();
}

private void DataReceiverLineBuffered()
{
try {
string x;
lock(this.readBuffer) {
x = this.readBuffer + this.port.ReadExisting();
}
char[] delim = new char[2] {'\n', '\r'};
while(x.Length > 1 && (x.Contains("\n") || x.Contains("\r"))) {
String[] ary = x.Split(delim, 2);
this.ReadHandler(ary[0], true);
x = ary[1] + this.port.ReadExisting();
}
lock(this.readBuffer) {
this.readBuffer = x;
}
} catch(Exception e) {
this.StatusReporter("Serial Read Error: " + e.ToString());
}
}

private void DataReceiverCharBuffered()
{
try {
string x;
lock(this.readBuffer) {
x = this.readBuffer + this.port.ReadExisting();
}
while(x.Length >= 1) {
string c = x.Substring(0, 1);
if(c == "\r" || c == "\n") {
this.unBuffNeedNewline = true;
x = x.Substring(1) + this.port.ReadExisting();
}
else if(this.unBuffNeedNewline) {
this.ReadHandler("", true);
this.unBuffNeedNewline = false;
} else {
this.ReadHandler(c, false);
x = x.Substring(1) + this.port.ReadExisting();
}
}
} catch(Exception e) {
this.StatusReporter("Serial Read Error: " + e.ToString());
}
}


The function "ReadHandler" takes two arguments: The string value that's read (without carriage return or linefeed characters) and a boolean value indicating whether the given string is followed by a newline or not. Further up the call chain the display function will take that information to display timestamps and do other per-line formatting fanciness. It's worth noticing that since we are calling from within an event handler, ReadHandler probably needs to call BeginInvoke to pass it's incoming data to the GUI (that's what my code does, anyway).

The way I lock the readBuffer repeatedly was something that I figured out later. When data is incoming quickly (like 115200 baud or higher) we can get multiple DataReceived events being triggered and stacking up if we lock through the entire function. This can cause noticeable program slowdowns when a number of event handlers acquire the lock subsequently. Instead, I lock smaller parts of the code and let additional handler instances run concurrently but with no input. Throughput performance is better in these cases, and is not noticeably different otherwise.

So that's my implementation of a buffered asynchronous serial port reader in C#. It's not perfect, but it gets the job done, has pretty good throughput, and is relatively fault tolerant.

Tuesday, May 5, 2009

Path to AIO on Parrot

In the last few blog posts I've put out about Asynchronous IO (AIO) on Parrot, I've looked at some issues including how things work on Windows and Linux, and how things could start looking on Parrot. The series started with this post and ends with this one. Notice that I haven't provided all the answers here, so I'm still hoping to get some feedback from readers. I'm also pretty well convinced that we will learn some lessons quickly as work progresses. The biggest hassle is going to be creating a system which is semantically transparent on Windows and Linux, so that PIR users don't need to care about the differences.

PDD22 contains some information about AIO although it's all speculative. I've been pouring over this document for a few days and I think I'm really understanding a lot of the nuances about it and some of the methodology that the original designers had in mind. I think it's a decent approach, and I think we will be able to follow it reasonably closely without too many issues popping up. I showed some speculative PIR examples in a previous blog post, but I'm starting to think now that I will follow the PDD more and pursue that idea less.

The PDD has this to say about the relationship between synchronous and asynchronous opcodes:
Parrot only implements synchronous I/O operations. Initially,the asynchronous operations will be implemented separately from the synchronous ones. There may be an implementation that uses one variant to implement the other someday, but it's not an immediate priority.
and it says this about how the interface to PIR will look:
Synchronous opcodes are differentiated from asynchronous opcodes by the presence of a callback argument in the asynchronous calls. Asynchronous calls that don't supply callbacks (perhaps if the user wants to manually check later if the operation succeeded) are enough of a fringe case that they don't need opcodes.
The main point to take away from this is that asynchronous IO requests are not implemented as a PMC (at least not as one that the user needs to be aware of) and they are not handled using Parrot's existing concurrency opcodes like schedule. Here's a PIR example for something that, I think, will perform an asynchronous file write according to the specification in the PDD:

$P0 = open "/path/to/file.txt", "w"
$P1 = find_global("io_callback_sub")
$P2 = print $P0, "hello world", $P1

In the example above, $P0 is the FileHandle PMC that we are writing to, $P1 is the callback subroutine, and $P2 is the IO request status object that keeps track of the status of the request. The PDD says that there should be a division between the synchronous and asynchronous opcodes. However, on Windows platforms at least the file handle will need to be opened in asynchronous mode before any asynchronous operations can be performed on it. I don't see any real way to avoid immediate unification in this case, unless:
  1. The print opcode closes the filehandle and reopens it in asynchronous mode (bad)
  2. We add an additional specifier to the open opcode that specifies that the file handle should be opened in asynchronous mode ("wn" would be "write, non-blocking", for instance).
  3. We always open file handles in asynchronous mode and just implement all the blocking opcodes in terms of underlying asynchronous operations.
I like idea #3 the best personally, but I imagine there is going to be some significant support for #2 as well. I'm just worried about cases where a non-asynchronous filehandle object is passed to an asynchronous opcode, or any other related combination of things that can and will happen.

Here are some notes that I'm following in general (I will discuss some caveats below):
  1. All streams are opened in asynchronous mode by default, except for streams that don't support it.
  2. Asynchronous opcodes will accept as a callback either a Sub, a Continuation, or PMCNULL. In the case of a Sub, the opcode will perform asynchronously and call the sub in a separate thread when an event occurs. If it's a Continuation, the operation will block until completed and then resume execution at the Continuation. If PMCNULL, it will launch the asynchronous request and ignore any results.
  3. We need to create a new PMC for the AIO status object. I'm thinking we call it "IORequest". The IORequest object will have interfaces to check for the current status (in progress, complete, error) and the result (number of bytes read/written on success). I am not sure how we will handle errors, there are a few options for this that I won't talk about here.
The problem with this approach that I can see is that nowhere in here do we interact with the concurrency scheduler at all, unless the C-level callback function of the request schedules the PIR-level callback Task instead of executing it directly, or for systems that don't support AIO directly and we need to fudge it. Relying on the direct callbacks is definitely better performance-wise then polling a result flag internally. However, I've been told now by a handful of people that the better way to go in Linux anyway is probably a poll loop using epoll anyway.

An alternative idea, that I personally like less but which might cause fewer headaches, would be to create the IORequest object as a simple flag accessor. The scheduler would need to poll the pending requests regularly (using epoll in linux and an IO Completion Port in Windows) and when it finds one that needs handling, it would update the flag in the IO request object and schedule the callback for us (if one is provided). The difference to this approach, of course, is who is checking the status and who is scheduling the callback (kernel vs Parrot). I feel like this way is going to have a lot of performance drawbacks, but then again the biggest performance drawback in any IO system isn't going to be the callback scheduler anyway. The differences might be negligible.

So we don't even know how we're going to do simple read/write operations yet, but we might be able to nail down the specifics of a few other tasks. A listener object or a Select/Poll object for instance might be registered with the scheduler to repeatedly check their status and call callbacks when an incoming event occurs. This would be useful on a server-side network app where we could listen on an array of sockets for incoming data passively and let the scheduler take care of checking flags and scheduling callbacks.

Here's a straight-forward example of code to create a passive listener (which some Unix/Perl folk would probably prefer we called "Select") PMC which calls a callback when an incoming event occurs:

$P0 = new 'Socket'
# ... Connect the socket here
$P1 = new 'IOListener'
push $P1, $P0
schedule $P1

So at the end of that snippet, we have a listener object in the the scheduler. The scheduler will poll the IOListener, which in turn will poll the Socket and all other PMCs that it contains, and for every event that it finds it will add the corresponding callback to the scheduler for execution.

So the PDD definitely offers a nice base to start working on, but there are a ton of questions to be answered about some of the implementation specifics. We'll know as we start breaking ground and writing code what does work and what doesn't. I'm sure I'll be reporting on these issues as they arise.

Monday, May 4, 2009

AIO On Linux

Yesterday I talked about the AIO situation on Windows. Today, as well as I am able, I am going to talk about the current situation on Linux. I've done asychronous AIO programming before on Windows, so that was an easy start for me. I've never used the Linux equivalents before, so this post is going to be a little bit more shakey.

First, let me start by saying that my initial findings on Linux AIO looked pretty bleak. I've seen some pages that talk about long lists of possible routes, each with unresolved problems. I've seen some project pages that look like they haven't been updated since 2007 or earlier. I've seen quotes like "This is stupid" or "That is crap". However, on closer inspection it's looking like there are some real possibilities for good AIO as of kernel 2.6. Specifically, the POSIX AIO API looks like it has a lot of promise for us. Unfortunately, a Google search for "Asynchronous IO on Linux" returns so much negative information.

I have found a good documentation page that does a decent job of talking about the various structures and functions involved in the POSIX API. However, I do notice a problem immediately that things like console IO cannot be accessed asynchronously. That's an issue that we can work around, however. Also, I notice that this POSIX API appears to use the one thing that I didn't like: Separate threads of blocking IO primitives running in user space. So, that's a negative, but not something we can't really get around here since the other options are worse. It's my understanding that the POSIX API is going to manage the synchronization issues, so maybe it's not such a bad thing.

In the last post I talked about adding an IO queue structure to the concurrency scheduler. Whenever we have a message, we schedule a handler task to process it. In Linux, we don't have anything like an IO Completion Port that can be used to manage a long list of requests for us (that I know of), but we can manually manage a list of struct aiocb structures and poll them for changes.

My interest has also been piqued by the aio_suspend function to help in implementing blocking IO operations internally. I also keep looking into the lio_listio function as a great way to launch a number of simultaneous requests, but I don't know how useful it will be in practice. We'll see how things fall into place one work begins on the actual implementation. One thing is for sure though: these POSIX functions don't appear to be available prior to the 2.6 Linux Kernel. So we're definitely going to need good configure probes, and we're going to need a good Plan B so we can simulate AIO on systems that don't support it directly.

Next up in my little series about AIO in Parrot will be a discussion about how we're going to be writing an AIO implementation that will be sane across Windows and Linux, and will properly integrate with the rest of Parrot. If you have any thoughts about this issue, or if you know anything about AIO on Darwin, please let me know. There are a lot of things that I don't know about these topics, so any input is much appreciated.

Sunday, May 3, 2009

AIO on Windows

I'm more familiar with Windows API programming then I am with the Linux Kernel, so I'm going to talk about the AIO implementation on Windows first.

In Windows, we have two basic methods for implementing AIO: Overlapped IO and Completion Ports. Overlapped IO uses the normal IO API (WriteFile, ReadFile, etc). The difference is that when we open a file handle we open it with the FILE_FLAG_OVERLAPPED flag. This tells the OS that all operations on this handle should be "overlapped", or asynchronous. We also have to create a special OVERLAPPED structure that includes information about timeouts and callbacks. Normal calls to WriteFile and ReadFile will then dispatch an asynchronous request instead of a synchronous one, so long as we also pass the OVERLAPPED structure to these calls.

An IO Completion Port is like a queue structure that contains and services many AIO requests. The completion port contains both a queue of AIO requests and a message queue. As requests are completed, messages are added to the message queue. The program can then poll this queue to determine which events have completed, if any. It's worth mentioning that IO Completion Ports can use callback routines too, so we don't lose that if we use completion ports.

In the Windows case it seems like it may be easier to retrofit the asynchronous operations into the FileHandle and Socket PMCs, instead of trying to create something new. We create a new FileHandle that is synchronous by default, although setting some kind of flag would open the handle instead as an overlapped handle. Maybe the simple act of setting a non-null callback function would cause this behavior. Of course, that doesn't really allow room for integrating AIO with Parrot's concurrency scheduler, and certainly isn't going to make things easy for having a smooth API that's usable with Windows AND Linux systems. So even though it seems like the more straight-forward method on Windows, I don't think it's going to be the way we should go in Parrot.

One thing that we could have instead is to add an "IO Request Queue" object to the scheduler, which in the Windows case would be an IO Completion Port structure. Asynchronous requests get added to the Queue, and the concurrency scheduler will regularly poll it to see when a message is received. When a message is received, the callback task is scheduled (or maybe even executed directly). There are lots of inefficiencies in this, and I don't have a lot of nice things to say about any system that blindly polls a flag, but it's a start for designing a unified AIO system.

So there are two basic methods that I can think of right now to implement an AIO system in Parrot. The first, as I mentioned above, uses a poll loop to keep track of completed IO events and schedules callbacks when they are received. The second, which I think I would like to avoid as much as possible is to use threading.

In a threaded AIO system, every new IO request launches a new thread. That worker thread executes various blocking operations, handles the callback, and then terminates. A big problem with this is that we can get into race conditions and data corruption issues if we launch two separate requests on the same IO target, unless we do lots of costly error checking and synchronization in Parrot. Instead, I think it's much better to let the OS's AIO API (now there's an alphabet soup for you!) handle the ordering and serializing of the requests.

I have to get some resources together and do a little research, but tomorrow or the day after I'll talk about the AIO situation on unixy systems too.

Update: I found some more interesting links:

Saturday, May 2, 2009

Using AIO from PIR

Last time I talked a little bit about how AIO would probably be implemented in PMC form, where each request or event was encapsulated in various PMC types. Let's take a quick look to explore how these might be used from PIR code.

First, a polled write:

.local pmc request
request = new 'AIOWriteRequest'
say request, "Hello World!"
poll_loop_top:
unless request goto poll_loop_top
say "Done!";

This would be, coincidentally, a way to implement a blocking call to the "say" opcode, in terms of an asynchronous variant. Of course, we would probably write it in C, but the steps would be the same. Now let's look at the same kind of idea, but using a callback to restore control flow instead of a polling loop to stall:

.local pmc request
request = new 'AIOWriteRequest'
.local pmc continue
continue = new Continuation
set_addr continue, resume_point
request.set_callback(continue)
say request, "Hello World Again!"
end

resume_point:
say "Done!"

A lot of this is speculative, I don't really know what would be considered the best way to halt the current execution thread but allow a scheduled task to continue execution later in the future. The "end" opcode is probably not a good one for this use, but I don't think any others exist. In either case, we probably want some kind of guarantee internally that all outstanding IO requests (besides a passive "listen") will be handled prior to interpreter termination, so calling "end" with an outstanding IO request could cause execution to resume as we expect. Again, speculative. Regardless of the specific details, this method performs the same blocking "say" call, but has the added benefit that it's not looping endlessly and eating resources the entire time. The currently running thread simply stops, and Parrot is free to use that time to do other work (such as handle the IO call).

I'm also ignoring some details about how buffering would work. For instance, if we were writing lots of little snippets to a file in a loop, would we buffer multiple snippets together into a single request, or would we dispatch each separately. Would we write:

loop_top:
$S0 = 'get_next_snippet'()
print requestobj, $S0 # Add $S0 to the buffer
unless ready_to_exit goto loop_top
schedule requestobj # Start the write operation with all snippets

Or maybe:

loop_top:
$S0 = 'get_next_snippet'()
print requestobj, $S0 # Set the payload
schedule requestobj # Schedule it, one snippet at a time
unless ready_to_exit goto loop_top

Obviously lots of questions to answer as we talk about implementing this system. One thing you will notice in the two previous examples, but you did not see in the first two was the use of the "schedule" opcode to actually dispatch the requests. For something like an asynchronous write operation, would the "say"/"print" opcodes actually schedule the IO, or would they just write data to the request buffer and then the "schedule" opcode would schedule it? Lots of questions to answer.

Let's look now at a passive listening PMC, such as a socket connection that would receive incoming data, or a PMC that listens to the OS to receive filesystem events:

.local pmc listener
listener = new 'AIOListen'
.local pmc callback
callback = find_global 'callback_sub'
listener.set_callback(callback)
listener.listen() # Could also be "schedule listener"
...

Every time the listener PMC received input, it would pass it along to the callback function:

.sub 'callback_sub'
.param pmc aiolisten # The AIOListen object
.param string data # The incoming data
...

In all the examples I've obviously left out some details. For instance, on none of the objects did I specify a filename or streamname, nor did I specify anything like a timeout, and I certainly didn't talk at all about buffering. Those details are necessary, but not important for these examples.

Here's another idea: What if instead of PIR-exposed AIO PMCs we had opcodes that managed these requests automatically?

writerequest filehandle, "Hello World!", callback1
readrequest filehandle, 20, callback2

And maybe an optional fourth argument would specify whether the current executing thread should suspend, terminate, or continue while the request is executing (which would enable us to do blocking IO very simply.

So these are some conceptual ideas about how AIO would be usable from PIR code. I'd be very interested to hear ideas that other people had about how it could be used. What I would like to see is example code that people think should work. Show me what you think these code examples should look like. There isn't anything firm on the design board yet, although I have a few more blog posts left to write about this topic.

Friday, May 1, 2009

AIO Variants

In a previous post I wrote a basic introduction to AIO and started alluding to some of the steps that would be needed to get a proper AIO system working in Parrot. The Wikipedia article on AIO mentions several different implementations or "flavors" of AIO that have been used throughout the years, and it should be apparent to most readers that several of these implementations are not useful for Parrot. However, I think there are three particular implementations that we probably do want to support in some fashion:
  1. Callbacks. These are probaby the most basic and also the easiest to implement directly in Parrot. An IO stream is opened and callbacks are provided to handle events. Events can be a variety of things, including the completion of a scheduled write request, the completion of a fixed-width read request, or the receipt of unscheduled incoming data. When the particular event is triggered, a callback subroutine is invoked to deal with it.
  2. Completion Ports: As I mentioned in the last post, completion ports use queued messages to indicate the completion of an IO task, instead of invoking a callback. The program must regularly poll the completion port to determine what, if anyhing, is happening. A completion port object with a queue of length one is identical to the VMS-style flagged IO
  3. Select/Poll Loops. These kinds of AIO are commonly used in networking applications like webservers, although there are many other uses for them as well. Select/Poll loops let us keep track of several IO streams through a single interface. When an event is triggered in any stream, the select/poll loop can pass control to it's handler. These can be implemented as an event loop over a series of filehandles, although there are plenty of risks for performance problems.
In Parrot, AIO jobs are likely to be encapsulated in a PMC type (or several types) and are likely to inherit from the Task PMC or Event PMC types. Each AIO task object will probably have to contain:
  1. The request itself, which is information about whether it's a read or a write, and the data to write or a count of characters to read.
  2. The destination of the IO. So, an AIORead PMC would contain a reference to a FileHandle PMC that would control where the data was coming from.
  3. A buffer or two to receive inputs and possibly to combine together outputs that have been made too closely together for the request to have been completed
  4. An optional callback object, which could be any invokable PMC such as Continuation, Sub, or Coroutine. There would likely be one callback object for each type of IO event. So there will be one callback for when a read completes, or when a write completes, or when information is received asynchronously without being requested.
  5. An internal flag or flags cache that can be polled to determine if an event has happened, if callbacks for those events have not been supplied.
It might make sense to have multiple AIO objects, such as an AIORead, AIOWrite, and AIOListen. This demarcation glosses over the need for bidirectional streams, but that could be resolved in a number of ways.

Asynchronous IO in Parrot

There are a few new features which are slated for implementation by Parrot 2.0, which is scheduled for release in January. That sure seems like a long way away, but it's never too early to be thinking about it. There are two large tasks that I'm particularly keen about working on: Garbage-collectable contexts and asynchronous IO. I'm waiting on the former because Allison Randal is doing some serious related work and I don't want to even think about it until her dust settles. The asynchronous IO (AIO) system in Parrot is something that I can start thinking about right now, so that's what I'm going to do.

When we think about normal IO, we think about calling a function that performs an IO operation and then returns when that operation is completed. Think about the "printf" function in C, or the "print" statement in Perl. Both of these are synchronous, or "blocking" IO calls. They take control away from your program, perform their operation, and then return when it is completed, blocking your program from continuing until they are finished. This can be significantly wasteful in some situations, although the effects can be mitigated through aggressive buffering.

AIO, in contrast to blocking IO, is a little different. Instead of making an IO call directly, you make a request for the system to perform the IO action. Control flow returns to your program immediately while the underlying system (usually the Operating System or the Virtual Machine) processes the request. On a multicore system, or just one that supports a good threading model, this can save a lot of time and resources. Plus, it lets your program start preparing the next request while the last one is processing.

Think back to your first class about programming, where the instructor probably drew out a quick diagram about the parts of the computer: The hard disks, the ram, the processor, etc. Storage in your computer moves from largest but slowest (hard disk) to smallest and fastest (processor registers). Writing to an uncached portion of memory can waste a few cycles, but writing to a poorly-buffered HDD can waste a hell of a lot more then that. And the entire time your drive is seeking, writing, and verifying (and maybe encrypting!) the data, your program needs to just sit and wait it out. At least, it needs to wait in a traditional blocking IO system. AIO systems leave the waiting to the operating system, and let your program get on with more important work.

When you make the request, the system usually creates a new thread to process it, or adds it into a queue for an existing thread, or something. The system executes the request on it's own time, and then it invokes a callback function to let your program know that the IO has been handled. Actually, this isn't always the case, users of recent Windows versions have probably heard of I/O Completion Ports, which are asynchronous IO objects that return status results in a queue that can be checked by your program, instead of invoking a callback. It's worth noting that a message-based AIO system could easily be built on top of a callback-based one, so Parrot could easily end up with both. This is yet another simplification on my part, there are several forms of AIO, many of which are described on Wikipedia.

So let's look at some implementation details as they pertain to Parrot:
  1. AIO is going to need some sort of asynchronous execution mechanism. Some operating systems provide an AIO API already that Parrot can piggyback on. Some systems might not, however (although I don't think any of our current target platforms fall into this category). If the OS doesn't have an AIO API available, we could put together our own using Parrot's threads implementation
  2. AIO is going to need a scheduling and dispatching mechanism, both to schedule the outgoing request and to schedule a callback when the request is complete. Parrot has a robust event scheduler already that we can use for this.
  3. At first you might think that we need two IO systems, one for the asynchronous and one for the blocking variants. However, this is not the case. All blocking IO calls can be implementing using AIO primitives. A blocking "print" call, for instance, can be implemented by scheduling an asynchronous write operation and then waiting on some kind of synchronizer (a spinlock or mutex or something). So, once we have AIO we only need to maintain one system (although that means a lot of Parrot's current IO system will need to be closely reviewed, if not reworked)
I'm going to be talking more about Parrot and AIO (and some other Parrot-related projects I have in my todo list) here in the next few days.