Looking for a design pattern

A

Alf P. Steinbach

* James Kanze:
* James Kanze:
* James Kanze:
[...]
It is, in fact, O(n)
when you go to the end of the file, which is a frequent
operation (since in many cases, that's where you're inserting
text).
Above is incorrect. Cursor gap is O(1) for moving to other
end. With any reasonable implementation, since going to the
ends are frequent operations.
It's certainly O(n) with the classical implementation of
cursor gap. Say, the one in emacs (or at least, the one in
emacs 15-20 years ago, which was the last time I looked at
the source).
You might as well say that quicksort is O(n^3) average time
because some novice has managed to implement something he
calls quicksort with cubic average time.

I wouldn't call the implementors of emacs novices. I'm talking
of actual implementations that I've seen, in real code.

The point stands regardless of what you'd like to call the implementors of emacs.

I've seen implementations of quicksort that didn't even work.

By your logic that means quicksort doesn't work. It's an invalid inference.
Whether or not you would call the programmer of such implementation a novice.



Cheers,

- Alf
 
J

James Kanze

[ ... ]
My point here is that a lot of editing is reading/moving
hence a lot of useless copy occur; while this would not be
normally a concern, it bothers me somewhat - perhaps a
belief I have to get rid of. All the most since
moving/writing are usually well defined phase of editing
(like in vi).
I think the problem is that you're more or less ignoring a
few basic facts, such as:
1) Locality of reference matters -- even a few references to
main memory are expensive compared to lots of references to
cache.
But to have something practical (I usually write quite a lot
of char when I write), I guess the gap size would exceed a
cache entry and you loose this advantage around the gap where
most of the operation occur and which, in definitive, is what
is displayed to the screen.

The cache will generally hold several pages, which don't have to
be contiguous. With the gap buffer, you have to deal with two
contiguous blocks, no more (and the cache will certainly hold
two entries, and the page size is likely to be more than you can
display at any one time, anyway). With the rope strategy, you
may have to hit many more than two separate blocks to refresh
the screen.

With both strategies, I'd almost certainly maintain a cached
image of the screen, only updating what needs to be updated in
it. Which means that in both cases, there probably won't be a
need to access more than one or two chunks, regardless of their
size.
I would not expect that in in multiple-core environment: write
is expensive because of synchronisation of memory and R/W
requires two bus access which is nowadays the bottleneck.

The processor doesn't wait for the write to finish; it pushes a
write request into the write pipeline, and goes on. This is
also true for a read, except that it must wait as soon as it
needs the value which was read.

When Jerry says "small chunks", he means very, very small
chunks. Probably 2 to 4 words at the most. Beyond that, the
processor is filling the pipeline faster than it can be emptied,
and you get a stall. (Note that very low level locality helps
here, as well. If you write a byte into a word, and there is
already a write to that word in the pipeline, the processor just
updates the request in the pipeline.)
 
J

James Kanze

[ ... ]
Those aren't what I'd consider prohibitive times. And the
simplest implementation is clearly the flat buffer, which is in
practice probably faster than the gap buffer for the most
frequent operations, and acceptably fast for the least frequent.
You could be right, but I'm a bit less convinced. About the
only operation for which it's faster is moving the cursor, but
it's not going to be a lot faster even for that. To move by
increments of something like a word or a line, you still have
to read through the data to find the next whitespace or
newline, so you haven't saved much even at best.
The only places I can see it saving a substantial amount of
time would be moving to an explicit position (e.g. a mark, or
beginning or end of file).

I don't know if the savings would be substantial, but anytime
your searching, and the cursor is in the middle of the search
space, a flat buffer will be faster; simply advancing to the
next character is significantly faster. If you're searching for
a single character, I doubt that it would make much difference,
but for a regular expression search, where you don't know in
advance the size of the match, you'll need some sort of iterator
which takes the gap into account, and that's more complicated
(and slower) than an iterator into flat memory (which can be
just a pointer). And depending on how regular expressions are
implemented, that might be a bottleneck operation; a regular
expression search can easily be O(n*m) in terms of incrementing
the iterator (where n is the size of the buffer, and m the
average number of characters matched before failure).
 
J

Jerry Coffin

[ ... ]
That's roughly Alf's suggestion, and it sounds likely to me,
except... emacs supports windows open on different X terminals,
and there's one active view per terminal. (I think---I don't have
the possibility of trying it at present.)

That could be handled any of a few different ways. One would be for
the buffer to allow multiple gaps, one for each window that's
currently active (you wouldn't normally expect more than a few
anyway...). Another would be to consider only one of the windows
truly active at any given time, and when/if a user on another
terminal did something, you'd make that the active window.

Even assuming it does support multiple windows having the focus on
the same document concurrently, it's a sufficiently unusual situation
that optimizing for it is almost certain to have very low priority...
 
J

Jerry Coffin

[ ... ]
Don't forget changing the active window or pane. If the cursor
is maintained as part of the buffer, then each time the active
view changes, it has to be updated.

True -- but when something like moving a window to the foreground
gets involved, the times just went up so much that almost anything
related to buffer handling is (at least usually) going to lose much
significance.
And as I pointed out, just naïvely using a flat buffer is even
faster:). All of these strategies were developed back at a
time when machines were a lot, lot slower.

Yes and no. You weren't really comparing apples to apples there. For
a flat buffer, moving the cursor is O(K) and inserting text is
roughly O(N) (where N=distance from cursor to end of text).

A split buffer reverses those: moving the cursor is roughly O(N)
(where N=distance of the move) and inserting text is O(K).

What you were comparing was, in essence, the speed of an insertion
with a flat buffer to the speed of a large cursor move with a split
buffer.

If you treat the situation as _purely_ a real-time problem (i.e. the
upper bound on the time for each operation is _all_ that matters)
then it seems likely that the flat buffer will generally be the
better choice.

At least for me, and I suspect for many others as well, that's not
exactly the expectation: while I expect essentially instantaneous
response time, I also expect that when I'm simply typing in text,
that it should take virtually no CPU time, so it has virtually no
impact on other things running in the background.

The other question is the percentage of cursor movement to actual
editing. That varies rather widely, and depends (among other things)
on the kind of editing at hand. For something like word processing,
there tends to be a fairly high percentage of actual editing, and
even when proofreading, making minor changes is still pretty common.

For a programming editor, there's an awful lot of the time that the
editor really acts as just a browser -- I might easily end up opening
and reading through half a dozen files before I make a really minor
change (at least in terms of the editing involved) in one part of one
of those files.

I wouldn't be surprised if the optimum for each was rather different.
 
J

Jerry Coffin

[ ... ]
I don't know if the savings would be substantial, but anytime
your searching, and the cursor is in the middle of the search
space, a flat buffer will be faster; simply advancing to the
next character is significantly faster. If you're searching for
a single character, I doubt that it would make much difference,
but for a regular expression search, where you don't know in
advance the size of the match, you'll need some sort of iterator
which takes the gap into account, and that's more complicated
(and slower) than an iterator into flat memory (which can be
just a pointer).

At least in most cases, it's fairly trivial to avoid that problem
entirely. At least with most versions I've used, an RE can only match
up to a single line. In that case, you save the current cursor
position, move the cursor to the beginning (or end) of the current
line, and then do two separate searches, one of the section of the
buffer before the split, and the other of the section of the buffer
after the split.

In theory, this could be a problem if the data didn't contain any
newlines -- but then again, the worst case move is the same as a
worst case move for inserting a character with a flat buffer...

In a typical case, however, the move involved is on the order of tens
of bytes or so -- so small that it's completely lost in the noise of
compiling the RE.
 
J

Jerry Coffin

(e-mail address removed)>, (e-mail address removed)
says...

[ ... ]
That depends on how you represent a position in the text.
With the gap buffer, it would be an offset; iterating on the text is
increasing the offset (minus the gap handling) With scratch buffer, it
would be a buffer and an offset in the buffer; iterating on the text
is end-buffer/next-buffer.

Example:

Step 1: Initial state
Buffer="This is a text"
Scratch=""
Text=[ptr=buffer+0,size=14]

Step2: Inserting " nice " before text
Buffer="This is a text"
Scratch=" nice"
Text=[buffer+0,9],[scratch+0,5],[buffer+9,5]

It is true that then going at a specif index in the text is in o(B)
where (B is the number of element in the list) but I expect M would be
small and if movements are small, it is negligible.

Hmm...you haven't specified what "M" is supposed to represent here,
so I'm not sure what you're trying to say. In the end, however, I
think we're left with a fairly simple situation: what you're calling
"negligible" here will usually be larger than what you're objecting
to in the case of a split buffer.
True. But mitigated by the fact that with the gap buffer, there is a
copy that requires moving things in main memory and I would expect it
to be at each move since the cache will likely be flushed, given the
interaction time.

First of all, I doubt that, at least with a modern processor. Most
processors now have at least a megabyte of cache, and five to ten
megabytes is probably closer to typical. Unless you have something in
the background that's really churning through a _lot_ of data, the
two cache lines used by a split buffer are fairly likely to survive a
task switch.

Second, even if we assume that all the data involved has to be read
from main memory, we're still stuck with the fact that a split buffer
is going to deal with two contiguous areas, where chained buffers
plus change buffers is going to deal with a larger number of disjoint
areas of memory, and (ultimately) is going to simply deal with more
total data -- virtually all the data being read with a split buffer
is the actual data itself, whereas the chained buffers plus change
buffer involves things like buffers containing not only the raw data,
but also pointers to other buffers.

[ ... ]
IMO, line representation is another issue. With gap buffer, the
position of char are moving while they don't with tree representation.

I have a hard time believing that this means much, at least under
most typical circumstances. If you really think you're going to see a
'goto line N' command often enough to bother, it's pretty easy to
optimize for it reasonably well in a split buffer. Simply put, you
create an index, but instead of storing line positions, you store
line sizes. To navigate to a particular line, you add up the sizes of
lines up to that one, and if that's greater than the current cursor
position, you add the gap size. This is still linear, but on the
number of lines rather than the number of characters.

With the tree representation, it really does change -- every time you
consolidate trees. From what you've said elsethread (and I'd tend to
agree) you're going to have to do that fairly frequently to avoid
other problems -- which means you're going to be updating your line
position data fairly frequently as well.
It is also true of many buffers. Isn't it ?

Yes and no. Yes, writes have short latency and reads have long
latency in both cases. The difference is that a multiple buffer setup
does more reading to avoid doing writes -- "penny wise and pound
foolish" as the old saying goes.
The same comment as above apply since it is the same argument.
I mentioned that the contiguous buffer would be reformed upon writing;
in most cases, I expect saves happens fairly often, in which case you
are in the same situation as with the gap buffer..

You're in the same situation -- but only by doing some arbitrary
amount of extra work that you're currently ignoring. To an extent,
that's fair -- the extra work can be done asynchronously. To another
extent, it's completely unfair. To make your design competitive, you
just about _have_ to do it asynchronously. That, in turn, means you
have to write the whole thing to be thread safe.

[ ... ]
But to have something practical (I usually write quite a lot of char
when I write), I guess the gap size would exceed a cache entry and you
loose this advantage around the gap where most of the operation occur
and which, in definitive, is what is displayed to the screen.

Not so. When you're inserting text, a gap buffer is dealing with
exactly one cache line, covering the space immediately after the
cursor. You only start to use a different cache line when the user
has inserted text that completely fills the current cache line.

In the end, you're always stuck with a few simple facts: a split
buffer spends most of its time dealing with one contiguous chunk of
memory. A multiple buffer/change buffer setup uses a bare minimum of
two separate areas of memory, _and_ it almost inevitably deals with
more raw data representing the same amount of real (user-visible)
data. That means it has to read more data to do its job.
I would not expect that in in multiple-core environment: write is
expensive because of synchronisation of memory and R/W requires two
bus access which is nowadays the bottleneck.

Write is expensive when and if you need synchronization. As noted
above, that's probably important for the design you're advocating,
but (at most) is much less so for the split-buffer design.

[ ... ]
I think you will agree that, given the power of computers, even
with a very poorly designed or over-consumptive editor, the user
experience will be the same so I don't think this is an issue here.

I agree that fast enough hardware can overcome a poor design, but I
don't think that's a good reason to create a poor design.
 
M

Michael Doubez

(e-mail address removed)>, (e-mail address removed)
says...
[ ... ]
That depends on how you represent a position in the text.
With the gap buffer, it would be an offset; iterating on the text is
increasing the offset (minus the gap handling) With scratch buffer, it
would be a buffer and an offset in the buffer; iterating on the text
is end-buffer/next-buffer.

Step 1: Initial state
Buffer="This is a text"
Scratch=""
Text=[ptr=buffer+0,size=14]
Step2: Inserting " nice " before text
Buffer="This is a text"
Scratch=" nice"
Text=[buffer+0,9],[scratch+0,5],[buffer+9,5]
It is true that then going at a specif index in the text is in o(B)
where (B is the number of element in the list) but I expect M would be
small and if movements are small, it is negligible.

Hmm...you haven't specified what "M" is supposed to represent here,

Oups M is B (I first wrote M for modification and replaced by B for
Buffer). I mean going at a specific position is linear with the number
of chunks.
so I'm not sure what you're trying to say. In the end, however, I
think we're left with a fairly simple situation: what you're calling
"negligible" here will usually be larger than what you're objecting
to in the case of a split buffer.

I can hear that.
First of all, I doubt that, at least with a modern processor. Most
processors now have at least a megabyte of cache, and five to ten
megabytes is probably closer to typical. Unless you have something in
the background that's really churning through a _lot_ of data, the
two cache lines used by a split buffer are fairly likely to survive a
task switch.

Second, even if we assume that all the data involved has to be read
from main memory, we're still stuck with the fact that a split buffer
is going to deal with two contiguous areas, where chained buffers
plus change buffers is going to deal with a larger number of disjoint
areas of memory, and (ultimately) is going to simply deal with more
total data -- virtually all the data being read with a split buffer
is the actual data itself, whereas the chained buffers plus change
buffer involves things like buffers containing not only the raw data,
but also pointers to other buffers.

Well. Actually, there is also only two buffers with the scratch
solution: one buffer holds the original data while the other holds all
the changes.
What might be a hit, regarding the cache, is the table holding the
pointers and size of each chunks and the indirection.
[ ... ]
IMO, line representation is another issue. With gap buffer, the
position of char are moving while they don't with tree representation.

I have a hard time believing that this means much, at least under
most typical circumstances.
Agreed.
[snip]

With the tree representation, it really does change -- every time you
consolidate trees. From what you've said elsethread (and I'd tend to
agree) you're going to have to do that fairly frequently to avoid
other problems -- which means you're going to be updating your line
position data fairly frequently as well.
True.

[snip]
I think you will agree that, given the power of computers, even
with a very poorly designed or over-consumptive  editor, the user
experience will be the same so I don't think this is an issue here.

I agree that fast enough hardware can overcome a poor design, but I
don't think that's a good reason to create a poor design.

My point exactly.

I start to like the gap solution, I appreciate how it balances the
time scale, usage and architecture.
 
P

Pascal J. Bourguignon

Jerry Coffin said:
For a programming editor, there's an awful lot of the time that the
editor really acts as just a browser -- I might easily end up opening
and reading through half a dozen files before I make a really minor
change (at least in terms of the editing involved) in one part of one
of those files.

I wouldn't be surprised if the optimum for each was rather different.

I would split the buffer at the cursor only when insertion or
suppression starts...
 
J

James Kanze

[ ... ]
I don't know if the savings would be substantial, but anytime
your searching, and the cursor is in the middle of the search
space, a flat buffer will be faster; simply advancing to the
next character is significantly faster. If you're searching for
a single character, I doubt that it would make much difference,
but for a regular expression search, where you don't know in
advance the size of the match, you'll need some sort of iterator
which takes the gap into account, and that's more complicated
(and slower) than an iterator into flat memory (which can be
just a pointer).
At least in most cases, it's fairly trivial to avoid that
problem entirely. At least with most versions I've used, an RE
can only match up to a single line.

That's generally true in line oriented editors, like vi(m). But
then, a line oriented representation is more appropriate than a
gap buffer for them anyway. It's not true in emacs, however,
and generally, in a non line oriented editor, the new line is
just another character, including in a regular expression.

Of course, if you mean that it is rare in practice for a regular
expression to actually contain something which matches a new
line, you may be right. (You're certainly right in my case, but
my use of regular expressions in editors is probably
conditionned by vi, to the extent that the idea of creating an
expression whose match includes a newline doesn't occur to me.)
But in order to take advantage of this fact, you have to analyse
the regular expression to figure out whether it can or cannot
match a new line (not too difficult, but extra work none the
less), and use two different matcher, depending on the case.
In that case, you save the current cursor position, move the
cursor to the beginning (or end) of the current line, and then
do two separate searches, one of the section of the buffer
before the split, and the other of the section of the buffer
after the split.
In theory, this could be a problem if the data didn't contain
any newlines -- but then again, the worst case move is the
same as a worst case move for inserting a character with a
flat buffer...
In a typical case, however, the move involved is on the order
of tens of bytes or so -- so small that it's completely lost
in the noise of compiling the RE.

It's not so much the move itself, but all of the necessary
additional logic to manage it. Frankly, given the performance
of modern processors, I'd simply go with a smart iterator; I
don't think the extra check each iteration, as to whether you're
at the gap or not would kill you.

Note too that editors normally use extended regular expressions
which allow capture of sub-strings. To my knowledge, those
can't be implemented as a pure DFA, which means that the actual
processing of each character probably overwhelms the additional
checking. Off hand, and this is just my basic intuition, and
not a result of any measurements, I would expect such a regular
expression to take about ten times more time scanning a large
range as it takes to copy it. (My own regular expression class
will take less time to scan than it takes to copy, because it
uses a DFA. But it doesn't support capture.)
 
J

Jerry Coffin

[ ... ]
Of course, if you mean that it is rare in practice for a regular
expression to actually contain something which matches a new
line, you may be right. (You're certainly right in my case, but
my use of regular expressions in editors is probably
conditionned by vi, to the extent that the idea of creating an
expression whose match includes a newline doesn't occur to me.)
But in order to take advantage of this fact, you have to analyse
the regular expression to figure out whether it can or cannot
match a new line (not too difficult, but extra work none the
less), and use two different matcher, depending on the case.

Yes, I'd say incrementing a pointer/subscript across the gap is only
worth doing if you can do so consistently. If you're going to deal
with incrementing across the gap at least part of the time, it
probably makes more sense to just do it all the time.

[ ... ]
It's not so much the move itself, but all of the necessary
additional logic to manage it.

If you supported REs that included newlines, I'd agree -- I wouldn't
advocate this technique in such a case. I doubt the speed gain would
be worth the extra complexity.

[ ... ]
Note too that editors normally use extended regular expressions
which allow capture of sub-strings. To my knowledge, those
can't be implemented as a pure DFA, which means that the actual
processing of each character probably overwhelms the additional
checking. Off hand, and this is just my basic intuition, and
not a result of any measurements, I would expect such a regular
expression to take about ten times more time scanning a large
range as it takes to copy it. (My own regular expression class
will take less time to scan than it takes to copy, because it
uses a DFA. But it doesn't support capture.)

Offhand, it seems like that _would_ probably justify the complexity
of scanning the RE to see if a DFA can be used or not, and using it
if possible. Then again, unless I had a really good reason to do
otherwise, I'd probably just use the regex class in TR1.
 
J

Jerry Coffin

[ ... ]
Yes, I'd say incrementing a pointer/subscript across the gap is
only worth doing if you can do so consistently.

Oops -- that should have said "... avoiding incrementing ..."
 
J

James Kanze

[ ... ]
It's not so much the move itself, but all of the necessary
additional logic to manage it.
If you supported REs that included newlines, I'd agree -- I
wouldn't advocate this technique in such a case. I doubt the
speed gain would be worth the extra complexity.

Since we're talking about editors here:): all regular
expression implementations I'm familiar with support newlines in
the expression. The data structure of some editors (ed, vi,
vim), and some other programs like those in the grep family, is
such, however, that they will only apply the regular expression
to the text one line at a time, to text which doesn't contain a
new line. (It's possible to create a regular expression in vim
which will only match if there is a newline in the text. Such a
regular expression will never match anything, however.)
[ ... ]
Note too that editors normally use extended regular
expressions which allow capture of sub-strings. To my
knowledge, those can't be implemented as a pure DFA, which
means that the actual processing of each character probably
overwhelms the additional checking. Off hand, and this is
just my basic intuition, and not a result of any
measurements, I would expect such a regular expression to
take about ten times more time scanning a large range as it
takes to copy it. (My own regular expression class will
take less time to scan than it takes to copy, because it
uses a DFA. But it doesn't support capture.)
Offhand, it seems like that _would_ probably justify the
complexity of scanning the RE to see if a DFA can be used or
not, and using it if possible. Then again, unless I had a
really good reason to do otherwise, I'd probably just use the
regex class in TR1.

Exactly. Typically, editors are not scanning millions of lines
of text; the amount of text being scanned is relatively small,
and the time to compile the regular expression is usually a
significant part of the time necessary for the command using it.
In such cases, building a complete DFA is counter productive
(because it is a fairly expensive operation); using lazy
construction of the DFA might be acceptable, but it does
introduce the added complexity of having to handle out of memory
in the middle of the search. But IMHO, tr1::regex should work
just fine, unless you need some special features.

(FWIW: I wouldn't use my own RegularExpression class in an
editor. It's not designed for that. On the other hand, it has
some nice features that boost::regex doesn't: you can define
your accept code, and or existing regular expressions, along the
lines of:
RegularExpression decimal ( "[1-9][0-9]*" , 10 ) ;
RegularExpression octal ( "0[0-7]*" , 8 ) ;
RegularExpression hexadecimal( "0[xX][a-fA-F0-9]+", 16 ) ;
RegularExpression number( decimal | octal | hexadecimal ) ;
and then:
StringMatch< std::string::const_iterator >
match( number.match( s.begin(), s.end() ) ) ;
if ( match ) {
int i
= convert( s.begin(), match.endOfMatch(), match.acceptCode
() ) ;
// ...
}
where the last argument to convert is the base. And the class
has a function "dumpAsCpp", which outputs the tables as C++, so
you can compile them into a StaticRegularExpression, which has
pretty much the same interface as RegularExpression for the
non-mutating functions, but is statically initialized. And
finally, the class(es) exist in two variants, supporting either
single byte encodings or UTF-8---although complex regular
expressions in UTF-8 can rapidly become very, very large---I use
the single byte variant a lot even when working with UTF-8.)
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,071
Latest member
MetabolicSolutionsKeto

Latest Threads

Top