16

I think I'm getting into such a specific question that there may be no answer, but it seems curious to me. This is a retrocomputing question, I promise, see the last paragraph to see how.

In Plauger's 1992 book, "The Standard C Library", Plauger gives reference implementations that work but are not meant to be the world's most efficient implementation. I'm implementing my own libc for kicks so I'm using this book a lot to understand it. I am also not going to try for super efficiency, for the record, and the reason I'm using Plauger's book is because I'm interested in a simpler C library than the ones that exist today.

One thing that stands out to me is that he uses for loops quite a bit where, to me anyway, a while loop seems more natural. Here is an example: his implementation of strcpy:

char* (strcpy)(char* s1, const char* s2)
{
    char *s = s1;
    
    for(s = s1; (*s++ = *s2++)!= '\0';)
        ;
        
    return (s1);
}

My own implementation, made before I referenced his, was almost identical but used a while loop:

char* strcpy(char* s1, char*s2)
{
    char* su1 = s1;
    const char* su2 = s2;
    
    while(*su1++ = *su2++);
    return s1;
}

So here's the retrocomputing part: Plauger was writing in 1992. Is there a specific reason that compilers at the time did better with for loops rather than while loops, or is this just a personal preference of Plauger?

17
  • 14
    Well, your solution is presented in Harbison and Steele's third edition from 1991, so it was pretty well known. So I'd vote for personal preference. Certainly nothing in the language was an issue.
    – Jon Custer
    Jun 22 at 13:30
  • 13
    Plauger's coding is sloppy. In the example, s1 is assigned to s twice: in the variable initialization, and in the for-loop initialization.
    – Leo B.
    Jun 22 at 15:41
  • 3
    When running these 2 implementations through the Godbolt compiler explorer, I could see no significant difference in the output asm that would make any performance delta, at least for gcc 4.8.5 on Power architecture and gcc 4.5.4 on ARM. However, 6502 cc65 results in a bit shorter code with the while loop, do not know if it is quicker.
    – Glen Yates
    Jun 22 at 15:59
  • 7
    @user3840170 - For me it comes from reading thousands of lines of code every day. In these sorts of complex operator-precedence situations I have to stop, tease apart what's going on, decide if this is a source of a problem, then continue on to the next line. IMMHO code needs to be written for the least capable person who will read it, not the most. Expecting every person reading the code to be a language-lawyer (or stop and look it up) is (again IMMHO) bad programming. Extra lines of simpler code is much quicker to read & debug than less lines of more complex code.
    – Kingsley
    Jun 23 at 5:12
  • 3
    I am in the same situation as you -- writing a C library, and using Plauger as an important reference on fine print that would probably have escaped my attention otherwise. I have to second Leo's sentiment: The code presented in the book exhibits all kinds of questionable habits, and could IMHO have been presented in a more legible way. It is definitely not a book on clean coding practices. That being said, it could also have been much worse, and is at least correct AFAICT.
    – DevSolar
    Jun 23 at 8:01

1 Answer 1

20

One thing that stands out to me is that he uses for loops quite a bit where, to me anyway, a while loop seems more natural.

That part is already discussed in chapter 3.5 Loops While and For on p. 56 of the very first edition of The C Programming Language:

We have already encountered the while and for loops. In

while (expression)
    statement

the expression is evaluated. If it is non-zero, statement is executed and expression is re-evaluated. This cycle continues until expression becomes zero, at which point execution resumes after statement.

The for statement

for (expr1 ; expr2 ; expr3)
    statement

is equivalent to

expr1;
while (expr2) {
    statement
    expr3;
}

Grammatically, the three components of a for are expressions. Most commonly, expr1 and expr3 are assignments or function calls and expr2 is a relational expression.

As to your question:

So here's the retrocomputing part: Plauger was writing in 1992. Is there a specific reason that compilers at the time did better with for loops rather than while loops, or is this just a personal preference of Plauger?

Yes, it's only a matter of style as K&R notes on p. 57 of their book with:

Whether to use while or for is largely a matter of taste. For example, in

while ((c = getchar()) == ' ' || c == '\n' || c == '\t')
     ; /* skip white space characters */

there is no initialization or re-initialization, so the while seems most natural.

The for is clearly superior when there is a simple initialization and re-initialization(*1), since it keeps the loop control statements close together and visible at the top of the loop. This is most obvious in

for (i = 0; i < N; i++)

which is the C idiom for processing the first N elements of an array, the analog of the Fortran or PL/l DO loop.

So Plauger might simply be used to a preference of for-loops shown by the base material and many many examples derived from it.

Take Away #1: Code writing is always a matter of style preference.

The book goes on a bit about further savings in source lines, showing a clear preference of K&R for writing tight source code, almost as readable as APL (but ... see below). Of course there could be other PoV.

The discussion about the merits of either version is already there in the very first book about C, more than a decade before Plauger's writing.

Take Away #2: Read the basics first is a great idea before switching to secondary literature :))

But there's also the but-part about some compiler shortfall: Some of these compacting line saving techniques were also hints for the compiler - or better ways to make less 'intelligent' compilers generate the best code. A line like *s1++ = *s2++ can be turned into an assignment with auto increment operations without much look ahead or reshuffling. Even better, on a CPU which sets the flags according to the last item moved, it spares an additional test for a trailing zero byte (*2).

So even the dumbest compiler, used for a CPU with auto increment and flags set by move will choose the best possible code - and that's what a PDP-11 was. It's one of the artifacts showing that while C may be seen as CPU independent, it was in all practical use constructed to best support a CPU with certain features.

This kind of compiler support became useless if not superficial the moment C was ported to different CPUs (*3) as well with more sophisticated compilers.

Take Away #3: There is a part of compiler hacking included in using such constructs. Even tough it was already way outdated when Plauger wrote his book.


A different POV about Style, readability and meaning.

In the mind of K&R for is kind of a combination of COBOL and APL. The readability of COBOL bay putting all items necessary for (basic) loops into a defined structure, so no need to look around for initialization while keeping it compact like some APL code.

But when looking at it in a more fundamental way I would consider both implementation presented in the question as bad implementation and note it in any code review.

To start with using while with anything but a test expression is using of side effects. By definition (check K&R) while is a repeating loop, one that is executed ZERO or more times. No execution of whatever it contains will ever happen if the condition is not true at the beginning of each iteration. Using it as shown goes against that basic principle.

After all, the task of copying a (zero) delimited string needs to transfer at least the trailing delimiter. So the task is to transfer 1..n elements, not 0..n.

Similarly, for does not fully fit the task, at least not as used. for defines an iteration as initialization, condition and reinitialization. Again none of the three should be manipulating anything outside of loop control. Except, when taking the items apart, using a for no longer satisfies the need for a 1..n loop - at least not without a lot of temporary variables/constructs.

The grammatically correct construct to describe this is a do/until as in do/while, so it should look rather like this:

  do
     st = *s1++ = *s2++;
  while (st != '\0');

Yes, I know, unfamiliar to classic C programmers drilled to save on lines at all cost.


*1 - Note the word re-initialization got changed to increment in the second edition.

*2 - This is BTW why Plauger writes (*s++ = *s2++)!= '\0' as that forces an explicit test for a zero value transfered, independent of compiler and CPU type. One step toward real portable code.

*3 - Including x86, which does not set flags according to moves.

15
  • 1
    @StephenKitt Thanks a lot for doing it twice. I learned something new: it needs only 3 ticks, ticks work inside of citations and there's a language keyword. Thanks.
    – Raffzahn
    Jun 22 at 15:49
  • 2
    retrocomputing.meta.stackexchange.com/questions/522/… where you argue against it ;-). Jun 22 at 16:20
  • 2
    the do/while code is incorrect. If the string is empty this is going to read out of bounds. Jun 22 at 16:27
  • 2
    The explicit comparison with zero has nothing to do with portability, beyond the fact that some compilers will issue a warning if an assignment operator is the outermost node of a condition-control expression, and some programmers configure compilers to treat warnings as errors. Many compilers will squawk at if (x=y) but accept if ((x=y) != 0) without complaint.
    – supercat
    Jun 22 at 17:24
  • 3
    @ilkkachu: Turbo C warned about that in the late 1980s.
    – supercat
    Jun 23 at 12:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.