Several weeks ago, I received an e-mail from a programmer who had written a syntax-highlighting editor using a Rich Edit control. It was inexplicably slow and he wanted to know how improve the performance of the syntax highlighting. Having received such queries before, I offered up my standard answers: Disable screen redraws during parsing and formatting, use Win32 API calls instead of TRichEdit properties and methods for fetching text from the control and setting character formats, and so on.
The programmer replied that he had done all of those things. I probably would not have given it any more thought had this programmer not mentioned that he had isolated the portion of the code that was consuming the bulk of the cycles. It was not in his syntax parser as I would have expected. Instead, the Win32 API EM_SETCHARFORMAT message alone seemed to be the culprit.
This puzzled me. Yes, I would expect applying color to the text to take a bit of time, but his experience was that this message was taking a surprisingly long time to execute. I did not give it much more thought until several weeks later when, as I was falling asleep, I thought I might have an answer.
To test my nocturnal revelation, I cobbled together a very small, and very flawed, syntax-highlighting editor. I assumed that most programmers would design their editors something like this:
OnChange handler.OnKeyDown handler.OnSelectionChange handler.With these design expectations and goals in mind, I threw together a quick program. First I created a new project and then added a main menu, a status bar with a couple of panels, and a TRichEdit. Next I added an OnKeyDown handler to the TRichEdit to track the insert/overwrite state:
void __fastcall TForm1::RichEdit1KeyDown(
TObject *Sender, WORD &Key,
TShiftState Shift)
{
// initialized to shift-state of none
static TShiftState ss;
static AnsiString Ins("INS");
static AnsiString Ovr("OVR");
if (Shift == ss && Key == VK_INSERT)
StatusBar1->Panels->Items[1]->Text =
(StatusBar1->Panels->Items[1]->Text
== Ins) ? Ovr : Ins;
}
I then added an OnSelectionChange handler to the TRichEdit to display in the status bar the number of the line containing the cursor:
void __fastcall TForm1::
RichEdit1SelectionChange(TObject *Sender)
{
StatusBar1->Panels->Items[0]->Text =
AnsiString("Line ") +
AnsiString(::SendMessage(
RichEdit1->Handle, EM_LINEFROMCHAR,
RichEdit1->SelStart, 0));
}
Next I added a Parse item to the main menu and an OnClick handler for the menu item. The OnClick handler kicks off a parse of the entire file by calling a function called ParseAllText(). I also added some code around this call to measure the effectiveness of optimizations.
The ParseAllText() function is excruciatingly simple. It walks through the text using the EM_FINDWORDBREAK message to break the text into tokens. It then calls a function called GetTokenColor() that looks up a token in a list and returns the associated color for the token. ParseAllText() then calls SetTokenColor() to set the text’s character format to the selected color. Here’s the SetTokenColor() function:
void SetTokenColor(TRichEdit* RichEdit1,
TEXTRANGE& tr, TColor color)
{
int selStart = RichEdit1->SelStart;
int selLength = RichEdit1->SelLength;
RichEdit1->SelStart = tr.chrg.cpMin;
RichEdit1->SelLength =
tr.chrg.cpMax - tr.chrg.cpMin;
RichEdit1->SelAttributes->Color=color;
RichEdit1->SelStart = selStart;
RichEdit1->SelLength = selLength;
}
Finally, I added an OnChange() handler for the TRichEdit to reparse lines whenever the user changes the text. My version is as lame as the ParseAllText() function mentioned earlier. It simply moves the cursor back by a couple of words and calls GetTokenColor() and SetTokenColor() for each of the next few words.
I timed several optimizations of my program, the results of which are presented in Table A. The baseline routine was, indeed, incredibly slow. Of course, the Rich Edit control is redrawing and scrolling every time the text or cursor position changed during the parsing/formatting pass so the lack of execution speed is not surprising.
Table A: Comparison of Optimization Routines
None (baseline) 189,363 100.0
Optimization 1 87,373 46.1
Optimizations 1 & 2 57,881 30.6
Optimizations 1, 2, & 3 10,207 5.4
Optimizations 1, 2, & 4 8,187 4.3
The first optimization is incredibly obvious. I simply disabled redrawing of the Rich Edit by wrapping the ParseAllText() call with WM_SETREDRAW messages:
::SendMessage(RichEdit1->Handle,
WM_SETREDRAW, false, 0);
ParseAllText(RichEdit1);
::SendMessage(RichEdit1->Handle,
WM_SETREDRAW, true, 0);
::InvalidateRect(
RichEdit1->Handle, 0, true);
As expected, this made a significant difference and cut the parsing time by more than half. However, I did not feel much of a sense of pride since it was such an obvious optimization.
The second optimization involves changing SetTokenColor() to use Win32 API calls instead of using the TRichEdit methods and properties to apply character formatting. After applying this code the function now looks like this:
void SetTokenColor(TRichEdit* RichEdit1,
TEXTRANGE& tr, TColor color)
{
CHARRANGE chrgSave;
::SendMessage(RichEdit1->Handle,
EM_EXGETSEL, 0, (LPARAM) &chrgSave);
::SendMessage(RichEdit1->Handle,
EM_EXSETSEL, 0, (LPARAM) &tr.chrg);
CHARFORMAT cf;
memset(&cf, 0, sizeof(cf));
cf.cbSize = sizeof(cf);
cf.dwMask = CFM_COLOR;
cf.crTextColor = color;
::SendMessage(RichEdit1->Handle,
EM_SETCHARFORMAT,
SCF_SELECTION, (LPARAM) &cf);
::SendMessage(RichEdit1->Handle,
EM_EXSETSEL, 0, (LPARAM) &chrgSave);
}
Again, this made a significant difference—about 1/3 faster than Optimization 1 alone. However, it is still a rather obvious optimization and one that I would have previously recommended.
These first two optimizations were all I had to offer my correspondent. It was not until I thought about how one might write a syntax-highlighting editor that I realized that the problem might not be in the Rich Edit control itself. Instead, it might be in the TRichEdit VCL component and, perhaps, in the manner that the programmer had structured his program.
Remember the OnSelectionChange and OnChange handlers that I mentioned earlier? These are called every time the parser moves the cursor or changes the character format. This happens regardless of whether we enable or disable screen redraws. Worse, my OnChange event handler is actually reparsing every time ParseAllText() changes the text format. Clearly, this was not a good design decision.
My next thought was to set these handlers to NULL before calling ParseAllText() and restore them upon return. Here’s the code:
RichEdit1->OnChange = 0;
RichEdit1->OnSelectionChange = 0;
RichEdit1->OnKeyDown = 0;
::SendMessage(RichEdit1->Handle,
WM_SETREDRAW, false, 0);
ParseAllText(RichEdit1);
::SendMessage(RichEdit1->Handle,
WM_SETREDRAW, true, 0);
::InvalidateRect(
RichEdit1->Handle, 0, true);
RichEdit1->OnChange = RichEdit1Change;
RichEdit1->OnSelectionChange =
RichEdit1SelectionChange;
RichEdit1->OnKeyDown = RichEdit1KeyDown;
Wow! As you can see from Table A, Optimization 3 cut the execution time to about 6% of the time for Optimizations 1 and 2 alone. Granted, this optimization simply fixes a flaw in the original program design (calling the OnChange handler when it is not needed). Maybe it was an obvious optimization, maybe not. Still, I was starting to feel a little proud of my efforts thus far. However, I found that I could do even better.
Understanding the last optimization requires a bit of insight into how Rich Edit controls provide notification when selection changes, text changes, and other events occur.
By default, Rich Edit controls do not provide any notification of such events. In order to get event notifications, you create an "event mask" specifying the events your program wants to be notified of. You then pass this mask to the Rich Edit control through the EM_SETEVENTMASK message. The LPARAM for this message is a mask with a bit set for each type of notification type that you wish to receive. For each enabled event, the Rich Edit control’s parent window will receive an EN_XXX message.
TRichEdit makes these events available to your program through the familiar OnXXX events. When a TRichEdit control is created (more accurately, when the underlying Rich Edit control’s window is created), TRichEdit sets the event notification mask to notify the control’s parent whenever the text is changed (the OnChange event), the selection is changed (the OnSelectionChange event), the window needs to be resized (the OnResizeRequest event), or whenever text is marked or unmarked as protected (the OnProtectChange event). For each of these events, the underlying Rich Edit control will send a notification message to the TRichEdit’s parent to tell it that the event occurred. The parent control will forward this message to the TRichEdit. The TRichEdit will then check to see if you have assigned a handler for the event and, if so, call it.
The key point here is that all of the above processing occurs whether or not your program has installed a handler for a specific event. The parent control is notified and it passes the message on to the TRichEdit control. The TRichEdit then checks for an installed handler. All of this takes time. In the previous optimization, we simply eliminated the custom event handlers from being called. Now, let’s eliminate the notification messages entirely.
To do this, I wrapped the ParseAllText() call with code to save the Rich Edit control’s event notification state, tell it not to generate notification messages, and restore the event notification state afterwards. I no longer need to disable the OnXXX handlers individually as I did in Optimization 3. The relevant code now looks like this:
int eventMask =
::SendMessage(RichEdit1->Handle,
EM_SETEVENTMASK, 0, 0);
::SendMessage(RichEdit1->Handle,
WM_SETREDRAW, false, 0);
ParseAllText(RichEdit1);
::SendMessage(RichEdit1->Handle,
WM_SETREDRAW, true, 0);
::InvalidateRect(
RichEdit1->Handle, 0, true);
::SendMessage(RichEdit1->Handle,
EM_SETEVENTMASK, 0, eventMask);
Table A shows that, using Optimization 4, the code executes in a fraction of the time required with only the first two optimizations. Further, it is 20% faster than optimization 3. The results are impressive for just a few lines of code.
The VCL TRichEdit component greatly simplifies coding reasonably sophisticated applications. However, it is designed for convenience, not for speed.
Simple and obvious optimizations (Optimizations 1 & 2) may significantly improve your syntax-highlighting editor’s performance. Careful attention to potential program design flaws (discovered in Optimization 3) is equally important. An understanding of the implementation of the TRichEdit component combined with careful study of the underlying Rich Edit control can improve your code even more as proven by Optimization 4.
For more information on Rich Edit controls and the TRichEdit component, see my Web site at http://home.att.net/~robertdunn/Yacs.html.