Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
PHP: why GOTO instead of recursion? (github.com/igorw)
84 points by napolux on Sept 23, 2014 | hide | past | favorite | 78 comments


So the short version is "PHP doesn't perform optimizations so I had to manually include them".


That whole thread made me cringe. Yes, it's a clever microoptimization, but he never mentions whether a profiler showed it to actually be beneficial. That might save 5 cycles out of a billion, for instance, at the cost of making the code "weird".

Don't try to out-clever the compiler. You might be able to do it, but there's no guarantee that tomorrow's compiler update won't be smarter than your today's workaround, leaving you with code that is both "weird" and slow.


> "there's no guarantee that tomorrow's compiler update won't be smarter than your today's workaround, leaving you with code that is both "weird" and slow."

Yeah, there is no guarantee, but let's be realistic here. That bug for PHP's incorrect parsing of octal integer literals has been unfixed for what, a decade now?


Point taken.


> Don't try to out-clever the compiler. You might be able to do it, but there's no guarantee that tomorrow's compiler update won't be smarter than your today's workaround, leaving you with code that is both "weird" and slow.

I think that's definitely true in the general case.

However, for a 19 line library whose focus is "fast retrying" where the goto construct is "goto beginning of function", I don't think it's really hugely "weird".

In this case it's also extremely unlikely to get slower.


> However, for a 19 line library whose focus is "fast retrying" where the goto construct is "goto beginning of function", I don't think it's really hugely "weird".

Fast retrying of network operations, generally. This optimization should save a few nanoseconds per call to a network service that might take a few hundred or thousand milliseconds. When the payoff is so vanishingly tiny as to be immeasurable, I'd take idiomatic code over clever interpreter-specific optimizations any day.


The most interesting thing about this thread (and the one on Github) is that so many programmers are now incapable of discerning between good use of goto, and bad use of goto. "goto is bad" has become a hard-line rule that programmers follow now without question or consideration.

Here's the current version of the retry function:

    function retry($retries, callable $fn)
    {
        beginning:
        try {
            return $fn();
        } catch (\Exception $e) {
            if (!$retries) {
                throw new FailingTooHardException('', 0, $e);
            }
            $retries--;
            goto beginning;
        }
    }
Now, throw out everything you know about gotos for a moment, and really look at the code. Is it unreadable? Hard to understand? Does it do anything wrong other than violate the hard rule, "thou shalt not goto"?

And if gotos are inherently, irredeemably evil, then why do we use switch statements? Because each case label in a switch statement is a goto.

Dijsktra in his original letter condemned the "unbridled" use of gotos. At that time, there was -- according to Dijsktra -- a growing trend in programming, where gotos were used to jump from one area of a program to the next, and into and out of loops, making the program flow very difficult to follow. (A good review of Dijsktra's "Goto considered harmful" is at http://david.tribble.com/text/goto.html .) Dijsktra proposed abolishing goto altogether as a knee-jerk reaction to the habits of that era's programmers, but only because in his view it was too great of a temptation for abuse. I doubt Dijsktra would have even raised an eyebrow at the use of goto in the example above.


It's not terrible, but it's obfuscating the fact that it's really just a do/while loop. It's not obvious at a glance what's happening because most peoples' eyes expect to see a for, or a do, or a while when they're trying to spot a loop.

It's also bad because you're effectively lying to the interpreter. Suppose a future optimization involves some super efficient loop unrolling or such. Well, too bad: because you've hand-rolled your own looping construct, the compiler probably won't be able to use it.

In general, just write idiomatic code that other programmers can readily work with. If (and only if!) a profiler identifies a problem, tweak just that part of the code. Leave liberal comments about what and why you did so that future programmers can evaluate whether your trickery is still worthwhile.


It really isn't that unclear, to me at least. If it's unclear to other programmers, I think that's only because gotos have fallen so far out of fashion that they look alien now. That's really a shame, because there are cases for the elegant use of goto, and I think the retry() code above is one of them.

You argued earlier that there was no point in making this optimization in the first place, since it probably wasn't contributing a significant delay in the application (which I disagree with on the grounds that we don't know what retry() might be used for), but now you're saying that it shouldn't be done this way because the compiler might later do a better job of optimizing a standard loop.

As others have pointed out, there are some good reasons not to expect PHP's bytecode runtime to match the optimizations of other software compilers. Without knowing in advance exactly how many times the function call embedded in retry() will be repeated -- which seems pretty unlikely -- there's no good way for the compiler to unroll the loop more efficiently than has already been done here with the straightforward application of a goto.

Sure, in general, don't be too clever. (Rather, "don't be cruel to future programmers".) I agree. But, I think there's a far greater danger of programmers blindly following guidelines like "don't use goto" without consideration.

So while there probably isn't a good argument for writing this function this way, there also isn't a good argument against it -- which is what makes all of the discussion around it so funny.


Sure looks like it to me. Compiler can't figure out how to replace 'true' with '\true'? Better do it myself.


In the context there was no compiler, there was an interpreter. PHP compilers (which do exist) might remove this type of redundancy, the PHP interpreter likely won't for reasons stated elsewhere in the comments.


But doesn't PHP compile the source code into byte code and then interpret that? The problem described here was in the _compiler_ part of PHP, not in the byte code interpreter.


All of which needs to occur after the user makes a request but before the request is returned.

Regardless of which word you wish to use (compiler, interpreter, lexer, etc) or which part of the process you wish to shift the workload into, that work (finding redundancies and removing them) is still being performed in real time after a request arrives (i.e. a user has to wait while we do this).

If the redundancy check costs more than the redundancy itself then removing them is utterly fruitless. The only way to avoid that is to take it out of that pipeline (i.e. pre-optimise the PHP OPCodes before a request arrives).

PHP pre-compilers and other optimisers absolutely exist and are readily used (e.g. HipHop). Within which case it might not matter if you used GOTO or While(true) as they'd both likely get optimised down to the GOTO OPCodes. However if you're using the original PHP interpreter then leaving them in might be the most optimal/cheap path to follow.


> The only way to avoid that is to take it out of that pipeline (i.e. pre-optimise the PHP OPCodes before a request arrives).

Most production installations of PHP pretty much mandate a bytecode cache and the official distribution bundles one since 5.5[0], so the redundancy check is done once but the optimisation works for any request thereafter.

> PHP pre-compilers and other optimisers absolutely exist and are readily used (e.g. HipHop).

HHVM is a completely separate VM which does not use (or consume, or care for in any way) the Zend bytecode produced by the Zend VM. HHVM has its own separate and incompatible bytecode format called HHBC[1]. It's neither a "pre-compiler" nor an "optimiser" for the Zend VM.

[0] http://php.net/manual/en/opcache.installation.php

[1] https://github.com/facebook/hhvm/blob/master/hphp/doc/byteco...


> HHVM is a completely separate VM... It's neither a "pre-compiler" nor an "optimiser" for the Zend VM.

"HHVM" is a VM. "HipHop [for PHP]" was a compiler[0], although it compiled down to C++ rather than Zend's VM (much like ShedSkin for Python[1])

[0] http://en.wikipedia.org/wiki/HipHop_for_PHP [1] http://en.wikipedia.org/wiki/Shed_Skin


> It's neither a "pre-compiler" nor an "optimiser" for the Zend VM.

Nobody claimed otherwise.


> Nobody claimed otherwise.

You provided HHVM as an example[0] of "PHP pre-compilers and other optimisers".

[0] that's literally what 'e.g.' means, it's an abbreviation for "exempli gratia", latin for "for example"


Indeed I did, and indeed it is.

You tacked on "for the Zend VM." Which was not something I said.

I said:

> PHP pre-compilers and other optimisers absolutely exist and are readily used (e.g. HipHop).

You then said:

> HHVM is a completely separate VM which does not use (or consume, or care for in any way) the Zend bytecode produced by the Zend VM. HHVM has its own separate and incompatible bytecode format called HHBC[1]. It's neither a "pre-compiler" nor an "optimiser" for the Zend VM.

Which is really answering a point nobody made. It is just completely out of the blue. My post literally does not contain the term "Zend." Nor does it make any claim to what OPCode formats different things use (and really the only reason you could even talk about HipHop was that it was given as a unqualified example).

So to this:

> It's neither a "pre-compiler" nor an "optimiser" for the Zend VM.

Is answering a point nobody made. It is like me saying, "You're wrong, the White House isn't blue, it is white!" When no claims to the blueness of the White House were made by yourself.


Why is this downvoted? it is a reasonable question.


Because its more aruging about semantics than anything else. At the end of the day, it needs to do this in real time and optimizing would cause a loss of speed for sometimes faster executed code, but since it all counts every time, it would be a waste.


Opcodes can be cached until the source file changes. There may be specific instances when caching cannot happen, but the poster seems to not be clear as to why that would apply in this case, hence the question.


PHP compilers are useful for optimizing SQL injection attacks.


Anywhere I can find out more about this?


Igorw's post uses the word compiler four times.


PHP compiles but acts as an interpreter. The Wikipedia article covers this[0]:

> The Zend Engine compiles PHP source code on-the-fly into an internal format that it can execute, thus it works as an interpreter.

I won't get drawn into a highly dull debate on the definition of what an interpreter is or isn't. Instead let's just agree that PHP by default generates the OPCodes every single time a request comes in. That's all we need to understand for this context (that OPCode generation is in-line with requests, not pre-compiled or otherwise cached).

Also, yes, there are tons and tons of implementations of PHP caching/precompilation/JIT/etc.

[0] https://en.wikipedia.org/wiki/PHP#Implementations


The original author wanted to generate byte code that executes as efficiently as possible. Clearly scoped goal. :)

Because PHP's compiler component, which he refers to as "compiler", did not generate efficient byte code for the interpreter, he had to use GOTO to force the compiler to generate it.

Whether the byte codes get cached or not might be irrelevant to his goals. We cannot be certain what proportion of time is wasted in network, compilation or serving the request. It could be one way or another.

USUALLY this is a waste of time. The developer's time that is.


I stand corrected. Compiler.


Compiler.


You should not be getting downvoted. The article is clearly discussing the compiler. Compilers and interpreters are not mutually exclusive. Just because a compiler produces bytecode (virtual instructions) instead of machine instructions (like x86), doesn't somehow make it not a compiler.


Exactly. A compiler transforms abstract source code into machine-readable instructions.

These machine-readable instructions can be executable directly, but not necessarily. Is a cross-compiler not a true compiler because you can't execute the resulting code directly? Not at all. So by the same token, one that produces byte code is no different.


I'll argue with myself here a bit if you allow... When considering PHP, we can think of The PHP Interpreter as a hybrid of two components: 1) a translator/compiler and 2) a bytecode interpreter. Therefore, when someone says "PHP Interpreter" he may indeed refer to that hybrid. If this is true, it would cause these kind of confusions. ;)


Yeah. I'd suggest that most "interpreters" today consist of a compiler and a virtual machine that runs those instructions. Historically, the earliest interpreted languages had a parser which immediately executed whatever it discovered, like BASIC, where each line was an independent statement that was parsed and executed.

It didn't take long for this to prove itself to be terribly inefficient.


I knew that the php interpreter was not very clever but the most surprising part is this one :

There is actually a difference between "while (true)" and "while (\true)", and it's not even capable to optimize this as a standard JMP. I can't believe that really simple things like this are still not optimized in the interpreter.


The key word there is interpreter. When you compile you have near infinite time to try and optimise and remove redundancy. When you interpret (i.e. real-time compilation) you have to decide if the redundancy check is more expensive than removing the redundancy.

Take this example, it is one extra OP Code instruction, which might generate a handful of ASM that actually gets run. How many ASM instructions would it take to detect this (and make sure there are no externalities) and remove it? I'd imagine considerably more.

And worse still you're also performing all of these redundancy checks even when there is nothing to remove. So now you've increased the cost to ALL loops, not just static ones with no purpose.

TL;DR: It is more expensive to detect and remove than to ignore.


> Take this example, it is one extra OP Code instruction, which might generate a handful of ASM that actually gets run. How many ASM instructions would it take to detect this (and make sure there are no externalities) and remove it? I'd imagine considerably more.

The detection is only done one while generating the bytecode, but the extraneous bytecode instructions have to be executed every time the loop… loops.

Not only that but it's a very simple optimisation to apply (even CPython is able to discover it).


> The detection is only done one while generating the bytecode

That isn't how PHP natively works. It is an interpreted language with compiler addons. It has to be done per every single request (as the CGI process may collapses after each one).

If you use something like HipHop or PHP accelerator then you can get cached OPCodes, and can also perform optimisation at that stage. But you cannot assume PHP will be executed in those environments.

PHP is not Java.


> That isn't how PHP natively works.

Yes it is.

> It has to be done per every single request

Yes? And the loop will most likely loop multiple times per request, which is my point.

> If you use something like HipHop or PHP accelerator then you can get cached OPCodes

HHVM is a completely separate and unrelated product. And you don't need "PHP accelerator" because the Zend VM bundles a bytecode cache since 5.5[0], not that it has any relevance to my point.

> But you cannot assume PHP will be executed in those environments.

Good thing I wasn't assuming anything then.

[0] http://php.net/manual/en/book.opcache.php


Wait, let's backtrack here.

I said that natively PHP doesn't cache and the OPCodes are generated every request. You said "Yes it is." meaning presumably you don't feel like that is the case.

You then go on to link to Zend Opcache which is, by all accounts, an extension which provides caching and which wasn't even bundled with PHP until 5.5 (according to yourself). So from 0.1 through 5.4 PHP shipped with zero caching and thus a core part of the language at that time was that it was generated every request.

Now, 5.5 was released in June 2013.

So for a lot of users PHP remains a language which is interpreted when a user makes a request. And even users on 5.5+, the first request per page is effectively interpreted as the cache isn't pre-populated.

So really what is it that we are even discussing? Seems like a debate about semantics at this stage.


> I said that natively PHP doesn't cache and the OPCodes are generated every request. You said "Yes it is." meaning presumably you don't feel like that is the case.

Since when do you backtrack to a completely different subtread (or to a subthread which does not exist)?

* I said that the detection (optimisation) would be performed while generating the bytecode

* You asserted that it's not how PHP works by default

* I replied that it is

You've asserted that Zend PHP does not use an intermediate bytecode during normal operation, do you stand by it? Because that's where we end up if we backtrack, not about the fact that it doesn't cache bytecode by default which is not actually relevant to my point (the existence and widespread use of bytecode cache merely supports said point by further improving the ROI).


> You've asserted that Zend PHP does not use an intermediate bytecode during normal operation, do you stand by it?

I never made that claim, and no I don't stand by a claim I never made.

You've asserted that PHP is actually JavaScript with style, do you stand by it?


> I never made that claim

Sorry to report that yes you did, let me quote exactly (rather than paraphrase):

>> The detection is only done one while generating the bytecode

> That isn't how PHP natively works.

Since "the detection" is obviously a hypothetical assumed in case the compiler ever got a peephole optimizer, the only part of the phrase you could object to is that Zend PHP generates intermediate bytecode.


Bear in mind any new optimizations would be added to 5.5+, which would presumably have opcode caching by default?

[Edit] Maybe a whole new world of possible optimizations has just opened up.


5.5 bundles an opcode cache in the default distribution, but the cache isn't enabled by default AFAIK.


To be clear, what optimization are you proposing? Replacing "JMPNZ true" with "JMP"?

So every time a loop is compiled (which by default is every request) there is the added overhead of "if opcode is JMPNZ and the first operand is the literal true, replace opcode with JMP and remove first operand".

This saves the "if operand is non-zero" check for all iterations of loops where the first operand is a constant "true".

Which means you've increased the cost of every loop on every request by a constant amount to decrease the cost of each iteration of "while (\true)".

Obviously you'd have to measure the actual costs, but it doesn't seem worth it to me.


> To be clear, what optimization are you proposing? Replacing "JMPNZ true" with "JMP"?

Yup. And remove FETCH_CONSTANT as well.

> So every time a loop is compiled (which by default is every request) there is the added overhead of "if opcode is JMPNZ and the first operand is the literal true, replace opcode with JMP and remove first operand".

Sure, or perform it while compiling the bytecode itself.

> This saves the "if operand is non-zero" check for all iterations of loops where the first operand is a constant "true".

It also saves loading the operand, because I expect most PHP code trying to loop forever doesn't use `\true`, and it can be applied to `for(;;)` to boot (a pretty common construct for those coming from C).

> Obviously you'd have to measure the actual costs

Obviously.


From the reply: "This requires doing a namespace lookup against igorw\true".

It has been a long time since I last wrote PHP code, but is it possible to dynamically redefine the value of `true`? That would (partially) explain why you can't statically evaluate it to `\true`.


> That would (partially) explain why you can't statically evaluate it to `\true`.

Yes, but that only requires a guard clause. That's what CPython 2 does in the same situation:

    >>> import dis
    >>> def foo():
    ...     while True:
    ...             print "ok"
    ... 
    >>> dis.dis(foo)
      2           0 SETUP_LOOP              15 (to 18)
            >>    3 LOAD_GLOBAL              0 (True)
                  6 POP_JUMP_IF_FALSE       17

      3           9 LOAD_CONST               1 ('ok')
                 12 PRINT_ITEM          
                 13 PRINT_NEWLINE       
                 14 JUMP_ABSOLUTE            3
            >>   17 POP_BLOCK           
            >>   18 LOAD_CONST               0 (None)
                 21 RETURN_VALUE        
(in Python 3, True and False are keywords so the loop prelude can be elided entirely)


I've never tried, but this (http://us2.php.net/manual/en/function.runkit-constant-redefi...) seems relevant.


So if you redefine true to false:

runkit_constant_redefine("true", false) == false in success and failure (as success results in true, which equals false now).


To add context, igorw is a contributor on php-internals has committed multiple patches/improvements[1] to php-src as recent as PHP 5.6

Great explanation!

https://github.com/php/php-src/commits?author=igorw


It seems to me that if you're this worried about performance you should be looking at something like HipHop.


[deleted]


15 years doesn't mean anything if it makes you inflexible. One GOTO statement in the right context doesn't make code unreadable. Programmers have been drilled since school never to use GOTO but if someone finds a valid case for why to use it, you as a programmer should be flexible enough to change.

Clojure another language that doesn't support tail recursion optimization (TRO) uses an ugly language construct called (recur) to mimic TRO which is essentially identical to a GOTO, so I don't see what the big deal is.

In the end, anything that can be done with tail recursion can be done in a loop which is way more readable then either using a GOTO or recursion. Tail recursion is only required for strict functional languages.


> In the end, anything that can be done with tail recursion can be done in a loop which is way more readable then either using a GOTO or recursion.

The real value in TCO isn't a function recurring on itself, it's mutual recursion.


No, that is not the only case where it has value.

TRO is a mandatory optimization for functional languages where recursion is the only possible looping construct. Haskell, Clojure, Erlang and Lisps rely on it heavily.


Sorry, I didn't mean it was the only value. It's the value that you get from it that you don't get from having loops.


Longest explanation of "function calls [and loops] are expensive" ever. Also the best example of why over-optimization is a completely valid thing to do if you're bored and have a debugger handy.


The explanation is mostly "Zend's bytecode compiler is retarded and can't do trivial optimisations so you have to handroll them".


In my experience, PHP can have really unexpected runtime performance. This site has some interesting benchmarks highlighting this: http://www.phpbench.com/

I'd like to hear of any other PHP performance gotchas people know of. One of my favorites: passing function arguments by value is faster than passing by reference.


The real question is: if you care that much about performance, why would you use the un-optimized PHP runtime?


Sometimes people optimize for fun, out of sheer curiosity, or some combination of things like that. Not everything we do as programmers has to be for some solely practical purpose. Having some fun with opcode optimization is certainly permissible and if you program a lot in PHP, learning how things work under the hood will make you a better PHP programmer in general.


I like including a few GOTOs just to piss off the Dijkstra acolytes, one of whom is quite vocal in the thread.


Since I see you're referencing my posts in that thread, you'll notice that I also point out that I understand that GOTO is not always bad, and, in fact, have used it before in production code.



If you need to use GOTOs in your code for speed and you're not coding in C, you should probably find a faster platform.


Not all useful control flow graphs can be described with while and if. It's perfectly fine to use goto. Look at some of the contortions people need to go through when avoiding it. Avoiding goto often makes programs harder to follow.

Why is it inexperienced programmers go through a phase where they think goto is evil incarnate? I wish Dijkstra had never written that damn paper.


>Not all useful control flow graphs can be described with while and if.

They can be described with if, while, for, case, pattern matching, recursion, etc. There is almost always a better alternative.

>Why is it inexperienced programmers go through a phase where they think goto is evil incarnate?

It seems to me that most people who support general usage of GOTO are inexperienced and don't necessarily realize it. They lack knowledge of better control structures. See the blub paradox.

Besides, the justification here is performance, not readability. I'm saying that performance in particular is a bad justification for using GOTO.


Agreed that sometimes it's right for control-flow purposes. Hence why I pointed out in the OP thread that I've used it before. But that's not the justification being used by the code author; his justification is purely performance.

>> "Why is it inexperienced programmers go through a phase where they think goto is evil incarnate? I wish Dijkstra had never written that damn paper."

You seem to be referencing my comments. I'm not inexperienced, and I don't think goto is evil incarnate. I do believe that optimizing for individual CPU cycles in PHP is generally pointless.


Languages without tail recursion elimination are very annoying. Looking at you JavaScript.


>> Languages without tail recursion elimination are very annoying. Looking at you JavaScript.

Programmers who use tail recursion instead of a loop are very annoying. Looking at you justincormack ;-)

Seriously, when I first saw recursion explained using the fibonacci sequence I was really scratching my head. I wondered why anyone would do that. Now that I'm older and wiser, I can see that it's natural to a mathematician. Why anyone who claims to be a programmer or compsci person would do it is beyond me. I suppose it's a simple example, but I'm a fan of introducing concepts with examples that really want the technique in question as a solution.


Some languages don't have loops, so you have to use tail recursion. Sometimes, tail recursion is more clear or concise than a loop:

Erlang:

  fib(1) -> 1;
  fib(2) -> 1;

  fib(N) when N > 2 -> fib(fib(1), fib(2), N - 2).
  fib(_A, B, 0) -> B;
  fib(A, B, N) when N > 0 -> fib(B, A + B, N - 1).
C:

  int fib(int count) {
     int a = 1, b =1;
     while (--count >= 2) {
        int next = a + b;
        a = b;
        b = next;
     }
     return b;
  }
Notes: Erlang version will crash if given value is less than 1; C version will just return 1 for anything outside of accepted range. Erlang integers become large integers automatically, C integers will overflow. If you prefer fib(0) == 0, you can adjust it, with more work for C, because I cheated the initial conditions/bounds check.

(I shrunk the C code a bit more than originally written, and it's now pretty similar in length... I guess it's about the same... which is the point :)


There are excellent uses. State machines as tail recursive functions are great, but in JavaScript you need a trampoline handler, which is much messier.


How about a depth-first search, then?


That's not tail recursion! It's tree recursion.


Well, good point, but so is the naive implementation of Fibonacci, isn't it?


Great read, but I think it's a few years too late. When you compare how PHP is trending against languages like Python it's following a similar path as perl, https://www.google.com/trends/explore#q=php%2C%20javascript%... - I'm surprised JavaScript isn't having much of an upturn lately.


On a side note, Monty Python (or the snake) are not programming languages. Check the "Related searches" section at the bottom and click "Python".


Silly me. I forget that not everybody lives in the world of development. Considering this it seems every language is experiencing a downturn. To me this looks more like a fault in Google's trend calculation. Maybe it doesn't consider search inflation from year to year.


Using the same kind of metric, certain male bodyparts seem to be trending as programming lang. as well.

https://www.google.com/trends/explore#q=php%2C%20javascript%...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: