배경HHVM은 페이스북이 개발한 고성능 PHP 가상머신으로, 공식보다 9배 빠르다고 합니다. 너무 궁금해서 잠깐 알아보고 이 글을 작성했습니다. 두 가지 질문에 답할 수 있기를 바랍니다.
어떻게 하시겠습니까?HHVM의 구현 원리를 논의하기 전에 다른 사람의 입장에서 생각해 보겠습니다. PHP로 작성된 웹사이트가 있고 분석 후 리소스의 상당 부분이 PHP에서 소비되는 것을 발견했다고 가정해 보겠습니다. PHP 성능을 최적화하시겠습니까? 예를 들어 여러 가지 방법이 있습니다.
옵션 1은 10년 전 넷스케이프의 예를 들어, 특히 페이스북처럼 복잡한 비즈니스 로직과 PHP 코드를 갖춘 제품도 포기할 것이라고 경고했습니다. 많은 경우 2,000만 라인이 있다고 합니다([PHP on the Metal with HHVM]에서 인용). 수정 비용은 아마도 가상 머신을 작성하는 것보다 더 높으며, 수천 명의 팀이 처음부터 학습하는 경우도 있습니다. 받아들일 수 없다. 옵션 2는 가장 안전한 솔루션이며 점진적으로 마이그레이션할 수 있습니다. 실제로 Facebook도 이와 관련하여 열심히 노력하고 있으며 Facebook에서 주로 내부적으로 사용하는 또 다른 RPC 솔루션인 Thrift와 같은 RPC 솔루션도 개발했습니다. 언어는 C인데, 이는 초기 Thrift 코드에서 볼 수 있는데, 다른 언어의 구현이 매우 조잡하여 프로덕션 환경에서 사용할 수 없기 때문입니다. 현재 Facebook에서는 PHP:C가 9:1에서 7:3으로 늘어났다고 합니다. Andrei Alexandrescu의 등장으로 C가 Facebook에서 점점 인기를 얻고 있지만 이는 문제의 일부만 해결할 수 있습니다. 결국 C는 PHP에 비해 개발 비용이 훨씬 높으며, 자주 수정되는 곳에서는 사용하기 적합하지 않으며, RPC 호출이 너무 많으면 성능에 심각한 영향을 미치게 됩니다. 옵션 3좋아 보이지만 실제로 구현하기는 어렵습니다. 일반적으로 성능 병목 현상은 그다지 크지 않습니다. 또한 비용이 지속적으로 발생합니다. PHP 확장 개발 수준이 높습니다. 이러한 종류의 솔루션은 일반적으로 공개 및 거의 변경되지 않은 기본 라이브러리에서만 사용되므로 이 솔루션으로는 많은 문제를 해결할 수 없습니다. 보시다시피 처음 세 가지 해결책으로는 문제를 잘 해결할 수 없기 때문에 페이스북은 실제로 PHP 자체의 최적화를 고려할 수밖에 없습니다. 더 빠른 PHPPHP를 최적화하고 싶은데 어떻게 최적화하나요? 제 생각에는 여러 가지 방법이 있습니다.
PHP 언어 수준의 최적화는 가장 간단하고 실현 가능합니다. 물론 Facebook은 이를 고려했으며 성능 병목 현상을 찾는 데 매우 유용한 XHProf와 같은 성능 분석 도구도 개발했습니다. 그러나 XHProf는 여전히 Facebook의 문제를 잘 해결하지 못했기 때문에 계속해서 살펴보겠습니다. 다음은 옵션 2입니다. 간단히 말하면 Zend의 실행 프로세스는 PHP를 opcode로 컴파일하는 부분과 opcode를 실행하는 부분으로 나눌 수 있습니다. 따라서 Zend 최적화는 이 두 가지 측면에서 고려될 수 있습니다. Opcode 최적화는 PHP의 반복적인 구문 분석을 피할 수 있는 일반적인 방법이며 Zend Optimizer Plus와 같은 일부 정적 컴파일 최적화도 수행할 수 있습니다. 그러나 PHP 언어의 동적 특성으로 인해 이 최적화 방법에는 제한이 있습니다. . 낙관적인 추정은 성능을 20%만 향상시킬 수 있습니다. 또 다른 고려 사항은 레지스터 기반 접근 방식과 같이 opcode 아키텍처 자체를 최적화하는 것이지만 이 접근 방식은 수정하기에는 너무 많은 작업이 필요하고 성능 향상은 특별히 눈에 띄지 않을 것이므로(아마도 30%?) 입출력 비율이 높지 않습니다. 또 다른 방법은 Opcode 실행을 최적화하는 것입니다. 먼저 Zend가 Opcode를 어떻게 실행하는지 간략하게 설명하겠습니다. Zend의 인터프리터(인터프리터라고도 함)는 서로 다른 Opcode에 따라 서로 다른 함수를 호출합니다. 하지만 설명의 편의를 위해 단순화한 것입니다. 그런 다음 이 함수에서 다양한 언어 관련 작업을 수행합니다(관심 있는 경우 "PHP Core 심층 이해" 책을 읽어보세요). Zend에는 복잡한 캡슐화와 간접 호출이 없습니다. 통역사에게는 아주 좋은 일입니다. Zend의 실행 성능을 향상시키려면 프로그램의 기본 실행을 이해해야 합니다. 예를 들어 함수 호출에는 실제로 오버헤드가 있으므로 인라인 스레딩을 통해 최적화할 수 있습니다. 그 원리는 C의 인라인과 같습니다. 언어는 같지만 런타임에 관련 기능을 확장한 후 순차적으로 실행하고(유추일 뿐이며 실제 구현은 다름) CPU 파이프라인 예측 실패로 인한 낭비도 방지합니다. 또한 어셈블리를 사용하여 JavaScriptCore 및 LuaJIT와 같은 인터프리터를 구현할 수도 있습니다. 자세한 내용은 Mike의 설명을 읽어보는 것이 좋습니다 그러나 이 두 가지 방법은 수정하기가 너무 비싸고 하나를 다시 작성하는 것보다 훨씬 더 어렵습니다. 특히 나중에 PHP의 특성을 언급할 때 알 수 있듯이 이전 버전과의 호환성을 보장하기 위해서는 더욱 어렵습니다. 고성능 가상 머신을 개발하는 것은 간단한 문제가 아닙니다. JVM이 현재 성능에 도달하는 데 10년 이상이 걸렸습니다. 그러면 이러한 고성능 가상 머신을 직접 사용하여 PHP의 성능을 최적화할 수 있을까요? 이것이 옵션 3의 아이디어이다. 사실 이 솔루션은 Quercus나 IBM의 P8 등 이전에도 시도된 적이 있으며, Quercus는 거의 누구도 사용하지 않았으며 P8도 종료되었습니다. 페이스북도 이 방법을 조사했고, 믿을 수 없는 소문까지 돌았으나 사실 페이스북은 2011년 포기했다. 옵션 3은 좋아 보이지만 실제 효과는 이상적이지 않기 때문에 많은 전문가(예: Mike)에 따르면 VM은 항상 특정 언어에 최적화되어 있으며 다른 언어는 이를 구현할 때 많은 병목 현상이 발생합니다. Dart 문서에 Dynamic method invocation이 소개되어 있고, Quercus의 성능은 Zend APC([from The HipHop Compiler for PHP])와 크게 다르지 않다고 되어 있어 별 의미가 없습니다. 근데 OpenJDK도 최근 몇 년간 열심히 노력하고 있는데 최근 Grall 프로젝트가 꽤 괜찮아 보이는데, 그 중에서 눈에 띄는 성과를 낸 언어도 있는데, Grall을 공부할 시간이 없었어요. , 그래서 여기서 판단할 수 없습니다. 다음은 옵션 4인데, 이것이 바로 HPHPc(HHVM의 전신)가 하는 일인데, PHP 코드를 C로 변환한 후 로컬 파일로 컴파일하는 것입니다(미리). ) 방법에 대한 기술적인 세부 사항은 The HipHop Compiler for PHP 논문을 참조하세요. 다음은 개요를 얻는 데 사용할 수 있는 논문의 스크린샷입니다. ![]() 는 최적화되어 있지만 PHP에서는 을 C 코드로 변환하는 예입니다. <div class="blockcode">코드 복사<div id="code_LjU">
<ol> <p> phc에 대해 말하자면, 저자는 2년 전 phc를 시연하기 위해 페이스북에 가서 그곳의 엔지니어들과 소통한 결과, 출시되자마자 인기를 끌었다고 자신의 블로그에서 눈물을 흘린 적이 있습니다. , 그리고 그는 4년 동안 바빴지만 어느덧 무명에 빠졌고 이제 그 미래는 암울해졌습니다. . . </p>
<p>Roadsend는 더 이상 유지 관리되지 않습니다. PHP와 같은 동적 언어의 경우 이 접근 방식에는 동적으로 포함될 수 없기 때문에 Facebook은 모든 파일을 함께 컴파일했으며 실제로 온라인에 접속할 때 파일 배포가 1G에 도달하고 있습니다. 받아들일 수 없다. </p>
<p>PHP QB라는 프로젝트도 있는데 시간 관계상 자세히 보지는 못했어요. </p>
<p>그래서 남은 방법은 더 빠른 PHP 가상 머신을 작성하고 이 어두운 길을 끝까지 가는 것입니다. 아마도 여러분도 Facebook이 가상 머신을 구축할 것이라는 소식을 처음 들었을 때 여러분도 저와 같을 것입니다. 너무 터무니없다고 생각했지만, 주의 깊게 분석해 보면 실제로는 이것이 유일한 방법이라는 것을 알게 될 것입니다. </p>
<h2>더 빠른 가상 머신</h2>
<p>HHVM이 더 빠른 이유는 무엇인가요? JIT의 핵심 기술은 각종 뉴스 보도에서 언급됐지만 사실 그렇게 단순하지는 않다. JIT는 단순히 파동만으로 성능을 향상시킬 수 있는 마술 지팡이도 아니고, JIT의 운영 자체도 시간이다. , 간단한 프로그램의 경우 인터프리터보다 느릴 수 있습니다. 가장 극단적인 예는 LuaJIT 2의 인터프리터가 V8의 JIT보다 약간 빠르기 때문에 세부 사항 처리에 관한 것이 아닙니다. .HHVM의 개발 역사 지속적인 최적화의 역사입니다. HPHPc를 조금씩 능가하는 모습을 아래 사진에서 보실 수 있습니다. </p>
<img alt="HHVM 是如何提升 PHP 性能的?" src="http://img.it-home.org/data/attachment/forum/2014pic/20140321154123_97.jpg" style="max-width:90%" style="max-width:90%">
<p>Android 4.4의 새로운 가상 머신 ART는 AOT 솔루션을 사용한다는 점은 언급할 가치가 있으며(기억하세요? 앞서 언급한 HPHPc는 이렇습니다) 결과는 JIT를 사용했던 이전 Dalvik보다 두 배 빠릅니다. 반드시 AOT보다 빠릅니다. </p>
<p>따라서 이 프로젝트는 매우 위험합니다. 강한 마음과 인내가 없으면 중도에 포기될 가능성이 높습니다. Google은 한때 Python의 성능을 향상시키기 위해 JIT를 사용하려고 했지만 결국 Google의 경우 사용에 실패했습니다. Python 여기에는 실제로 성능 문제가 없습니다. Google은 Python으로 크롤링을 작성했지만 [In The Plex 참조] 1996년에는 그게 전부였습니다. </p>
<p>Google에 비해 Facebook은 분명히 더 큰 동기와 결단력을 가지고 있습니다. PHP는 Facebook의 가장 중요한 언어입니다. Facebook이 이 프로젝트에 투자한 전문가는 누구인지 살펴보겠습니다. </p>
</ol>
<ul>
<li>, <c>의 저자이자 C 분야의 명가인 안드레이 알렉산드레스쿠</c>
</li>
<li>Keith Adams는 VMware의 핵심 아키텍처를 담당했습니다. 당시 VMware에서는 그를 단독으로 파견하여 Intel과 기술 협력을 진행했는데, 이는 그가 VMM 분야에 대해 얼마나 많은 지식을 가지고 있는지 입증했습니다.</li>
<li>Microsoft에서 .NET 가상 머신 개발에 참여하여 JIT를 개선한 Drew Paroski</li>
<li>Jason Evans, Firefox의 메모리 사용량을 절반으로 줄인 jemalloc 개발</li>
<li>"PHP 확장 및 내장"의 저자이자 PHP 커널 전문가인 사라 골레몬(Sara Golemon)은 PHP 마스터라면 누구나 이 책을 읽었을 것이라고 생각합니다. 어쩌면 그녀가 실제로 여성인지는 모를 수도 있습니다</li>
</ul>
<p> 비록 Lars Bak, Mike Pall과 같은 가상머신 분야의 최고 전문가는 없지만 이들 전문가가 함께 일할 수 있다면 가상머신을 작성하는 데에는 큰 문제가 없을 것입니다. 그렇다면 그들이 직면하게 될 과제는 무엇일까요? 다음으로 하나씩 논의해 보겠습니다. </p>
<h3>사양은 어떻게 되나요? </h3>
<p>PHP 가상 머신을 직접 작성할 때 직면해야 하는 첫 번째 문제는 PHP에는 언어 사양이 없고 여러 버전 간의 구문이 호환되지 않는다는 것입니다(5.2.1 및 5.2.3과 같은 작은 버전 번호라도). PHP 언어 사양 어떻게 정의하나요? IEEE의 성명을 살펴보겠습니다. </p>
<blockquote>
<p>PHP 그룹은 (언어) PHP 사양에 대한 최종 결정권을 갖고 있다고 주장합니다. 이 그룹 사양은 구현일 뿐이며 전문적인 사양이나 합의된 검증 제품군은 없습니다.</p>
</blockquote>
<p>그래서 Zend의 구현을 솔직하게 살펴보는 것이 유일한 방법입니다. 다행히 HPHPc에서는 한 번 고생한 적이 있어서 HHVM이 직접 사용할 수 있으므로 이 문제는 그리 크지 않습니다. </p>
<h3>언어 또는 확장자? </h3>
<p>PHP 언어를 구현하는 것은 가상 머신을 구현하는 것만큼 간단하지 않습니다. PHP 언어 자체에는 다양한 확장 기능도 포함되어 있으며 Zend는 사용할 수 있는 다양한 기능을 구현하기 위해 끊임없이 노력하고 있습니다. PHP 코드를 분석해 보면 빈 줄 주석을 제외하고 C 코드에 80만 줄이 있다는 것을 알 수 있는데, Zend 엔진 부품이 몇 개나 있는지 짐작해 보세요. 100,000개 미만의 행이 있습니다. </p>
<p>이것은 개발자에게는 나쁜 일이 아니지만, 엔진 구현자에게는 매우 비극적인 일입니다. Java 가상 머신을 작성하려면 대부분 바이트코드 해석과 일부 기본 JNI 호출만 구현하면 됩니다. Java의 내장 라이브러리는 Java로 구현되므로 성능 최적화를 고려하지 않으면 작업량 측면에서 JVM보다 PHP 가상 머신을 구현하는 것이 훨씬 어렵습니다. 예를 들어 누군가는 TypeScript를 8,000줄로 구현했습니다. JVM Doppio. </p>
<p>이 문제에 대한 HHVM의 해결 방법은 매우 간단합니다. 즉, 페이스북에서 사용하는 것만 구현하고, HPHPc에서는 이전에 작성한 것을 그대로 사용할 수도 있으므로 문제는 크지 않습니다. </p>
<h3>통역사 구현</h3>
<p>다음 단계는 PHP를 파싱한 후 HHVM에서 설계한 바이트코드를 생성하고 이를 <code>~/.hhvm.hhbc (SQLite 파일)에 저장하여 재사용할 때 Zend와 유사합니다. 바이트코드는 다른 기능으로 구현됩니다(이 메소드는 가상 머신에서 특별한 이름을 가집니다: 서브루틴 스레딩)
Interpreter의 본체는 bytecode.cpp에 구현되어 있습니다. <div class="blockcode">
<div id="code_oM7"><ol>
<li>if (c2.m_type == KindOfInt64) return o(c1.m_data.num, c2.m_data.num);</li>
<li>if (c2.m_type == KindOfDouble) return o(c1.m_data.num, c2.m_data.dbl);</li>
</ol></div>
<em onclick="copycode($('code_oM7'));">复制代码</em>
</div> HHVM이 HPHPc에 비해 PHP 구문에 대한 지원을 크게 향상시킨 것은 바로 Interpreter 때문입니다. 이론적으로는 공식 PHP와 완벽하게 호환되지만 이것만으로는 성능이 크게 향상되지는 않습니다. Zend. 변수 유형을 결정할 수 없기 때문에 위와 유사한 조건문을 추가해야 합니다. 그러나 이러한 코드는 최신 CPU의 실행 최적화에 도움이 되지 않으며, 데이터가 각 읽기에 필요하다는 것입니다. 이와 같은 문제는 JIT에 의존하여 최적화해야 합니다. JIT 구현 및 최적화우선, PHP의 JIT가 이전에 시도된 적이 없다는 점을 언급할 가치가 있습니다.
그럼 JIT란 정확히 무엇인가요? JIT를 구현하는 방법은 무엇입니까? 동적 언어에는 기본적으로 실행을 위해 문자열을 전달할 수 있는 평가 메서드가 있습니다. JIT는 문자열이 아닌 다른 플랫폼의 기계 코드를 연결한 다음 실행해야 한다는 점을 제외하면 비슷한 작업을 수행합니다. , 하지만 C로 구현하는 방법은 무엇입니까? 다음은 기사의 코드 일부입니다. <div class="blockcode">
<div id="code_JSG"><ol>
<li>부호 없는 문자 코드[] = {</li>
<li> 0x48, 0x89, 0xf8, // mov %rdi, %rax</li>
<li> 0x48, 0x83, 0xc0, 0x04, // $4 추가, %rax</li>
<li> 0xc3 // ret</li>
<li>};</li>
<li>memcpy(m, code, sizeof(code));</li>
</ol></div>
<em onclick="copycode($('code_JSG'));">코드 복사</em>
</div> 그런데 기계어 코드를 손으로 작성하다 보면 실수하기 쉽기 때문에 Mozilla의 Nanojit, LuaJIT의 DynASM 등 보조 라이브러리를 갖는 것이 가장 좋은데, HHVM은 이들을 사용하지 않고 라이브러리를 구현한다. x64(ARM 64비트를 생성하기 위해 VIXL도 사용하려고 함)의 경우 mprotect를 사용하여 코드를 실행 가능하게 만듭니다. 그런데 왜 JIT 코드가 더 빠른가요? 사실 C로 작성된 코드는 결국 기계어 코드로 컴파일되는데, 같은 코드를 그냥 수동으로 기계어 코드로 변환한다면, GCC에서 생성한 것과 무슨 차이가 있을까? 앞에서 CPU 구현 원리를 기반으로 한 몇 가지 최적화 기술을 언급했지만 JIT에서 더 중요한 최적화는 유형에 따라 특정 명령을 생성하여 명령 수와 조건부 판단 수를 크게 줄이는 것입니다. TraceMonkey의 다음 그림은 이를 매우 직관적으로 비교합니다. 나중에 HHVM에서 구체적인 예를 살펴보겠습니다. ![]() HHVM은 처음에는 인터피터를 통해 실행되는데, 언제 JIT를 사용하게 되나요? 두 가지 일반적인 JIT 트리거 조건이 있습니다.
두 가지 방법 중 어느 것이 더 나은지에 대해 다양한 전문가, 특히 Mike Pall(LuaJIT 작성자), Andreas Gal(Mozilla VP) 및 Brendan Eich(Mozilla CTO)의 논의를 불러일으킨 Lambada 게시물이 있습니다. 내 의견을 많이 표현했고 모두에게 시청을 권장했습니다. 여기서는 부끄러움을 과시하지 않겠습니다. 두 가지의 차이점은 컴파일 범위뿐 아니라 지역 변수 처리와 같은 많은 세부 사항에 있는데 여기서는 다루지 않습니다 그러나 HHVM은 이 두 가지 방법을 사용하지 않고 대신 유형에 따라 구분되는 Tracelet이라는 방법을 만들었습니다 ![]() 함수를 3개 부분으로 나누는 것을 볼 수 있습니다. 위쪽 2개 부분은 물론 고성능 JIT를 구현하기 위해서는 다양한 시도와 최적화가 필요합니다. 예를 들어 처음에는 HHVM에서 추가한 트레이스릿이 맨 앞에 배치됩니다. 즉, 위 그림에서 A와 C의 위치가 됩니다. 나중에 넣어보려고 했는데 테스트 결과 응답형을 미리 맞추는 게 더 쉽다는 걸 발견해서 성능이 14%나 향상됐어요 JIT의 실행 프로세스는 먼저 HHBC를 SSA(hhbc-translator.cpp)로 변환한 다음 SSA(예: 복사 전파)를 최적화하고 이를 X64 아래의 Translator-x64와 같은 로컬 기계어 코드로 다시 생성하는 것입니다. 구현되었습니다. HHVM이 최종적으로 생성한 기계어 코드가 어떤 것인지 알아보기 위해 다음 PHP 함수와 같은 간단한 예를 들어보겠습니다. <code class="php language-php" data-lang="php"><div class="blockcode">
<div id="code_B9S">
<ol>
<li><?php<li>function a($b){<li> echo $b 2;<li>}</ol></div><em onclick="copycode($('code_B9S'));">复制代码</em></div> 함수 a($b){<div class="blockcode"> echo $b 2;<div id="code_ZLy">}<ol><li><em onclick="copycode($('code_B9S')));">코드 복사<li><li><li> <li>컴파일 후 다음과 같습니다. <li>
<li><li><li><li><li>mov rcx,0x7200000<li>mov rdi,rbp<li>mov rsi,rbx<li>mov rdx,0x20<li>call 0x2651dfb <HPHP:: Transl::traceCallback(HPHP::ActRec*, HPHP::TypedValue*, long, void*)></li>
<li>cmp BYTE PTR [rbp-0x8],0xa</li>
<li>jne 0xae00306</li>
<li>; 매개변수 확인 </li>
<li>
<li>mov rcx,QWORD PTR [rbp-0x10]; 여기서 %rcx에는 1의 값이 할당됩니다.</li>
<li>mov edi,0x2; %rdi의 비트) 2</li>
<li>add rdi,rcx; add %rcx</li>
<li>call 0x2131f1b <HPHP::print_int(long)> 이번에는 첫 번째 매개변수 %의 값을 호출합니다. rdi는 이미 3입니다</li>
<li>
<li>; 나중에 논의하지 않습니다</li>
<li>mov BYTE PTR [rbp 0x28],0x8</li>
</ol>lea rbx,[rbp 0x20]</div>test BYTE PTR [r12],0xff<em onclick="copycode($('code_ZLy'));">jne 0xae0032a</em>push QWORD PTR [rbp 0x8]</div>mov rbp,QWORD PTR [rbp 0x0]mov rdi,rbpmov rsi,rbxmov rdx,QWORD PTR [rsp ]call 0x236b70e <HPHP::JIT::traceRet(HPHP::ActRec*, HPHP::TypedValue*, void*)>ret 코드 복사
HPHP::print_int 함수의 구현은 다음과 같습니다. <div class="blockcode"><div id="code_K6f">
<ol>
<code class="c language-c " data-lang="c "><div class="blockcode">
<div id="code_K6f"><ol>
<li>void print_int(int64_t i) {</li>
<li> char buf[256];</li>
<li> snprintf(buf, 256, "%" PRId64, i);</li>
<li> echo(buf);</li>
<li> TRACE(1, "t-x64 output(int): %" PRId64 "n", i);</li>
<li>}</li>
</ol></div>
<em onclick="copycode($('code_K6f'));">复制代码</em>
</div> void print_int(int64_t i) { char buf[256]; TRACE(1, "t-x64 출력(int): %" PRId64 "n", i); }<div class="blockcode">
<div id="code_K70"><ol><li>-v Eval.JitWarmupRequests=0</li></ol></div>
<em onclick="copycode($('code_K70'));">复制代码</em>
</div> 코드 복사 HHVM으로 컴파일된 코드는 를 직접 사용하여 인터프리터에서 매개변수를 판단하고 간접적으로 데이터를 얻어야 하는 문제를 피함으로써 성능을 크게 향상시키고 최종적으로는 C로 컴파일된 코드는 크지 않습니다. |