My experience making an on-disk merge sort using ranges
Chris Cain
clcain at uncg.edu
Tue Feb 26 17:47:11 PST 2013
Greetings everyone,
First, let me apologize that this is quite a long post. I hope
that it doesn't scare you off.
I wasn't exactly sure where to put this because I have the
intention of learning more about D and its overall design as well
as to let others know about some various difficulties I've been
experiencing while using it for various purposes and point out
potential fixes. As such I was torn between the D.Learn forum and
the vastly more observed D forum. I hope you don't mind my
eventual decision on the matter.
Let me say that I was inspired to try out the "Component
Programming" aspect of D using its ranges because of the
excellently done recent talk by Walter. As such, I'm mostly
interested in perfecting this particular approach. Obviously, I
have no doubt that I could program this in other ways and have no
issues whatsoever.
--
So, with that said, let's get to my point:
My goal is to write an on-disk merge sort (nearly) exclusively
using ranges. The part of the problem I'm highlighting in this
post is the process of merging multiple sorted files together
into one file (in this case, stdout). I claim that this code
should be acceptable:
---
import std.stdio, std.file, std.algorithm, std.conv;
void main() {
dirEntries("data", "*.out", SpanMode.shallow)
.map!(e => File(e.name).byLine(KeepTerminator.yes)
.map!(l => l.to!string())()
)()
.array()
.nWayUnion()
.copy(stdout.lockingTextWriter);
}
---
I was certainly surprised to find out that nWayUnion existed in
std.algorithm which essentially does the work for me. However, I
was more surprised to find out that this does not compile. So,
who here thinks this _shouldn't_ work? Look at it pretty closely
and, without investigating the error, justify to yourself why
this shouldn't work before you continue on. I will explain how I
perceive the problem.
I thought I was already way ahead of the game by using to!string
on each line in byLine (because I'm aware of its oddities because
it gives you the actual buffer it writes to). Honestly, I was a
bit shocked to discover that such a simple approach won't work
because moveFront doesn't work with MapResult. Keep in mind,
there are two maps here, but the MapResult that causes a problem
is the *inner* one (the one which is applied on each line of the
file).
---
Okay, so with that out of the way, I'd like to know what the
idiomatic way to solve this particular problem is. Here's what I
did (I don't claim the ranges I created are robustly implemented):
https://gist.github.com/Zshazz/c488e70eee4fd352b789
The first thing I figured out was that if you turned the
MapResult into an array (using .array()), then it would work as
expected. However, this is obviously not an acceptable solution
to my problem because I'm doing an on-disk merge sort to sort
things that wouldn't work in RAM. So, finding this out, I
realized that the (likely) problem with MapResult is that it's a
value type and that prevents moveFront from being applicable to
it for some reason (I'm unknowledgable to that reason
unfortunately).
My second solution was to write a wrapper for it that turns it
into a reference type (just made a quick class that forwards
calls to MapResult which it holds a copy of) to test my theory.
Sure enough, my wrapper worked as expected. However, I compared
it to another program and found it to be much slower, so I
assumed it might be because of the forwarding mechanism slowing
it down.
My third solution was to write a reference-based map. I couldn't
make a value type of map that would work because I didn't know
what prevented it from playing nicely with moveFront. This
approach was much faster and actually managed to match my
hand-written merge code. I was pretty satisfied with that
approach, but it bothered me that I was essentially duplicating
the functionality provided by std.algorithm.
My final solution was born out of me spending more time looking
at the reason why my refMap was faster. I discovered that when I
implemented refMap, I cached the result of applying the function
on front (which differs from the map implementation in
std.algorithm). So, I decided to rewrite my original wrapper so
that it caches the result. This provided me the performance
benefits of my refMap, but I didn't feel guilty about duplicating
existing functionality. So, my caching range is my favorite
solution thus far. Still, I would have much rather have had my
original attempt work and a caching range be invented soley to
improve performance, not to just get it to work at all.
---
With all of that in mind, my question is this: Why doesn't
MapResult work with moveFront? Also, if it cannot be made to work
with moveFront as a value type, would it be a good idea to turn
it into a reference type? Is there any way to make it transparent
to the user so that they don't have to do this sort of
investigation to solve what should be a fairly simple problem
(merging multiple ranges together, in this case)?
Thank you for taking the time to read this,
Take care.
More information about the Digitalmars-d
mailing list