Cuộc phỏng vấn tuyệt vời với Cliff Click, cha đẻ của việc biên dịch JIT trong Java

Cuộc phỏng vấn tuyệt vời với Cliff Click, cha đẻ của việc biên dịch JIT trong Javacú nhấp chuột vào vách đá — CTO của Cratus (cảm biến IoT để cải tiến quy trình), người sáng lập và đồng sáng lập của một số công ty khởi nghiệp (bao gồm Rocket Realtime School, Neurensic và H2O.ai) với một số lần thoát ra thành công. Cliff viết trình biên dịch đầu tiên của mình ở tuổi 15 (Pascal cho TRS Z-80)! Ông được biết đến nhiều nhất với công trình về C2 ở Java (Biển nút IR). Trình biên dịch này đã cho thế giới thấy rằng JIT có thể tạo ra mã chất lượng cao, đây là một trong những yếu tố đưa Java trở thành một trong những nền tảng phần mềm hiện đại chính. Sau đó, Cliff đã giúp Azul Systems xây dựng một máy tính lớn 864 lõi bằng phần mềm Java thuần túy hỗ trợ tạm dừng GC trên vùng 500 gigabyte trong vòng 10 mili giây. Nói chung, Cliff đã cố gắng làm việc trên tất cả các khía cạnh của JVM.

 
Habrapost này là một cuộc phỏng vấn tuyệt vời với Cliff. Chúng ta sẽ nói chuyện về các chủ đề sau:

  • Chuyển sang tối ưu hóa ở mức độ thấp
  • Cách thực hiện tái cấu trúc lớn
  • Mô hình chi phí
  • Đào tạo tối ưu hóa cấp độ thấp
  • Ví dụ thực tế về cải thiện hiệu suất
  • Tại sao tạo ngôn ngữ lập trình của riêng bạn
  • Sự nghiệp kỹ sư hiệu suất
  • Những thách thức kỹ thuật
  • Một chút về phân bổ đăng ký và đa lõi
  • Thử thách lớn nhất trong cuộc đời

Cuộc phỏng vấn được thực hiện bởi:

  • Andrey Satarin từ Dịch vụ web của Amazon. Trong sự nghiệp của mình, anh ấy đã cố gắng làm việc trong các dự án hoàn toàn khác nhau: anh ấy đã thử nghiệm cơ sở dữ liệu phân tán NewSQL trong Yandex, hệ thống phát hiện đám mây trong Kaspersky Lab, trò chơi nhiều người chơi trong Mail.ru và dịch vụ tính giá ngoại hối trong Deutsche Bank. Quan tâm đến việc thử nghiệm các hệ thống phân tán và phụ trợ quy mô lớn.
  • Vladimir Sitnikov từ Netcracker. Mười năm làm việc về hiệu suất và khả năng mở rộng của NetCracker OS, phần mềm được các nhà khai thác viễn thông sử dụng để tự động hóa các quy trình quản lý mạng và thiết bị mạng. Quan tâm đến các vấn đề về hiệu suất của Cơ sở dữ liệu Java và Oracle. Tác giả của hơn chục cải tiến hiệu suất trong trình điều khiển JDBC PostgreSQL chính thức.

Chuyển sang tối ưu hóa ở mức độ thấp

Andrew: Bạn là một tên tuổi lớn trong thế giới biên dịch JIT, Java và công việc hiệu suất nói chung, phải không? 

vách đá: Nó là như thế đấy!

Andrew: Hãy bắt đầu với một số câu hỏi chung về hiệu quả công việc. Bạn nghĩ gì về sự lựa chọn giữa tối ưu hóa cấp cao và cấp độ thấp như làm việc ở cấp độ CPU?

vách đá: Vâng, mọi thứ ở đây đều đơn giản. Mã nhanh nhất là mã không bao giờ chạy. Vì vậy, bạn luôn cần phải bắt đầu từ trình độ cao, làm việc trên các thuật toán. Ký hiệu O tốt hơn sẽ đánh bại ký hiệu O kém hơn trừ khi có một số hằng số đủ lớn can thiệp. Những thứ cấp thấp sẽ đi sau cùng. Thông thường, nếu bạn đã tối ưu hóa đủ tốt phần còn lại của ngăn xếp và vẫn còn một số nội dung thú vị thì đó là mức thấp. Nhưng làm thế nào để bắt đầu từ cấp độ cao? Làm thế nào để bạn biết rằng công việc cấp cao đã được thực hiện đủ? Chà... không thể nào. Không có công thức nấu ăn làm sẵn. Bạn cần hiểu vấn đề, quyết định xem bạn sẽ làm gì (để không thực hiện các bước không cần thiết trong tương lai) và sau đó bạn có thể khám phá trình hồ sơ, có thể nói điều gì đó hữu ích. Đến một lúc nào đó, bản thân bạn nhận ra rằng mình đã loại bỏ những thứ không cần thiết và đã đến lúc thực hiện một số điều chỉnh ở mức độ thấp. Đây chắc chắn là một loại hình nghệ thuật đặc biệt. Có rất nhiều người làm những việc không cần thiết nhưng lại di chuyển quá nhanh khiến họ không có thời gian để lo lắng về năng suất. Nhưng điều này là cho đến khi câu hỏi được đặt ra một cách thẳng thắn. Thông thường 99% không ai quan tâm đến việc tôi làm, cho đến thời điểm có một việc quan trọng xảy ra trên con đường quan trọng mà không ai quan tâm. Và ở đây mọi người bắt đầu cằn nhằn bạn về việc “tại sao nó không hoạt động hoàn hảo ngay từ đầu”. Nói chung, luôn có điều gì đó cần cải thiện về hiệu suất. Nhưng 99% trường hợp bạn không có khách hàng tiềm năng! Bạn chỉ đang cố gắng làm một việc gì đó thành công và trong quá trình đó bạn tìm ra điều gì là quan trọng. Bạn không bao giờ có thể biết trước rằng tác phẩm này cần phải hoàn hảo, vì vậy, trên thực tế, bạn phải hoàn hảo trong mọi việc. Nhưng điều này là không thể và bạn không làm điều đó. Luôn có rất nhiều thứ cần khắc phục - và điều đó hoàn toàn bình thường.

Cách thực hiện tái cấu trúc lớn

Andrew: Bạn thực hiện buổi biểu diễn như thế nào? Đây là một vấn đề xuyên suốt. Ví dụ, bạn đã bao giờ phải giải quyết các vấn đề phát sinh từ sự giao thoa của nhiều chức năng hiện có chưa?

vách đá: Tôi cố gắng tránh nó. Nếu tôi biết hiệu suất sẽ là một vấn đề, tôi sẽ nghĩ đến điều đó trước khi bắt đầu viết mã, đặc biệt là với cấu trúc dữ liệu. Nhưng thường thì bạn phát hiện ra tất cả những điều này rất muộn. Và sau đó bạn phải đi đến những biện pháp cực đoan và làm cái mà tôi gọi là “viết lại và chinh phục”: bạn cần lấy một mảnh đủ lớn. Một số mã sẽ vẫn phải được viết lại do vấn đề về hiệu suất hoặc lý do khác. Dù lý do viết lại mã là gì đi nữa thì việc viết lại một đoạn mã lớn hơn là một đoạn mã nhỏ hơn hầu như luôn luôn tốt hơn. Lúc này, mọi người bắt đầu run lên vì sợ hãi: “Ôi chúa ơi, bạn không thể chạm vào nhiều mã như vậy được!” Nhưng trên thực tế, cách tiếp cận này hầu như luôn mang lại hiệu quả tốt hơn nhiều. Bạn cần phải giải quyết ngay một vấn đề lớn, vẽ một vòng tròn lớn xung quanh nó và nói: Tôi sẽ viết lại mọi thứ bên trong vòng tròn. Đường viền nhỏ hơn nhiều so với nội dung bên trong cần được thay thế. Và nếu việc phân định ranh giới như vậy cho phép bạn thực hiện công việc bên trong một cách hoàn hảo thì bạn sẽ được tự do, hãy làm những gì bạn muốn. Khi bạn đã hiểu được vấn đề, quá trình viết lại sẽ dễ dàng hơn nhiều, vì vậy hãy cố gắng nhiều nhé!
Đồng thời, khi bạn viết lại một lượng lớn và nhận ra rằng hiệu suất sẽ là một vấn đề, bạn có thể ngay lập tức bắt đầu lo lắng về nó. Điều này thường biến thành những điều đơn giản như “không sao chép dữ liệu, quản lý dữ liệu đơn giản nhất có thể, làm cho nó nhỏ gọn”. Trong các bản viết lại lớn, có những cách tiêu chuẩn để cải thiện hiệu suất. Và chúng hầu như luôn xoay quanh dữ liệu.

Mô hình chi phí

Andrew: Trong một trong những podcast bạn đã nói về các mô hình chi phí trong bối cảnh năng suất. Bạn có thể giải thích ý của bạn về điều này?

vách đá: Chắc chắn. Tôi sinh ra trong thời đại mà hiệu năng của bộ xử lý cực kỳ quan trọng. Và thời đại này lại quay trở lại - số phận không phải không có sự trớ trêu. Tôi bắt đầu sống trong thời đại của máy tính 256-bit; chiếc máy tính đầu tiên của tôi hoạt động với XNUMX byte. Chính xác là byte. Mọi thứ đều rất nhỏ. Các hướng dẫn phải được tính toán và khi chúng tôi bắt đầu di chuyển lên ngăn xếp ngôn ngữ lập trình, các ngôn ngữ ngày càng được sử dụng nhiều hơn. Có Trình biên dịch mã, rồi đến Cơ bản, rồi đến C và C đảm nhiệm rất nhiều chi tiết, như phân bổ đăng ký và lựa chọn lệnh. Nhưng mọi thứ đều khá rõ ràng ở đó và nếu tôi tạo một con trỏ tới một thể hiện của một biến, thì tôi sẽ nhận được tải và chi phí của lệnh này sẽ được biết. Phần cứng tạo ra một số chu kỳ máy nhất định, do đó tốc độ thực hiện của nhiều thứ khác nhau có thể được tính toán đơn giản bằng cách cộng tất cả các lệnh mà bạn sắp chạy. Mỗi so sánh/kiểm tra/chi nhánh/cuộc gọi/tải/lưu trữ có thể được cộng lại và cho biết: đó là thời gian thực hiện đối với bạn. Khi nỗ lực cải thiện hiệu suất, bạn chắc chắn sẽ chú ý đến những con số tương ứng với các chu kỳ nóng nhỏ. 
Nhưng ngay khi bạn chuyển sang Java, Python và những thứ tương tự, bạn sẽ nhanh chóng rời xa phần cứng cấp thấp. Chi phí gọi một getter trong Java là bao nhiêu? Nếu JIT trong HotSpot đúng nội tuyến, nó sẽ tải, nhưng nếu nó không làm điều này thì nó sẽ là một lệnh gọi hàm. Vì cuộc gọi nằm trong vòng lặp nóng nên nó sẽ ghi đè tất cả các tối ưu hóa khác trong vòng lặp đó. Vì vậy, chi phí thực tế sẽ cao hơn nhiều. Và bạn ngay lập tức mất khả năng xem một đoạn mã và hiểu rằng chúng ta nên thực thi nó về tốc độ xung nhịp của bộ xử lý, bộ nhớ và bộ đệm được sử dụng. Tất cả điều này chỉ trở nên thú vị nếu bạn thực sự hòa mình vào màn trình diễn.
Bây giờ chúng ta đang ở trong tình thế mà tốc độ xử lý hầu như không tăng trong một thập kỷ. Ngày xưa đã trở lại! Bạn không còn có thể tin tưởng vào hiệu suất đơn luồng tốt nữa. Nhưng nếu bạn đột nhiên dấn thân vào lĩnh vực điện toán song song thì khó vô cùng, mọi người đều nhìn bạn như James Bond. Sự tăng tốc gấp mười lần ở đây thường xảy ra ở những nơi mà ai đó đã làm hỏng điều gì đó. Đồng thời đòi hỏi rất nhiều công việc. Để tăng tốc gấp XNUMX lần, bạn cần hiểu mô hình chi phí. Cái gì và nó có giá bao nhiêu? Và để làm được điều này, bạn cần hiểu lưỡi phù hợp với phần cứng cơ bản như thế nào.
Martin Thompson đã chọn một từ hay cho blog của mình Sự đồng cảm cơ học! Bạn cần hiểu phần cứng sẽ làm gì, chính xác nó sẽ làm như thế nào và tại sao ngay từ đầu nó lại làm những gì nó làm. Bằng cách sử dụng tính năng này, khá dễ dàng để bắt đầu đếm các lệnh và tìm ra thời gian thực hiện. Nếu bạn không được huấn luyện phù hợp thì bạn chỉ đang tìm kiếm một con mèo đen trong phòng tối. Tôi thấy mọi người luôn tối ưu hóa hiệu suất nhưng không biết mình đang làm cái quái gì. Họ đau khổ rất nhiều và không tiến bộ nhiều. Và khi tôi lấy cùng một đoạn mã, thực hiện một vài thủ thuật nhỏ và được tăng tốc gấp năm hoặc mười lần, họ sẽ nói: à, điều đó không công bằng, chúng tôi đã biết bạn giỏi hơn. Tuyệt vời. Tôi đang nói về cái gì vậy... mô hình chi phí là về loại mã bạn viết và tốc độ trung bình của nó trong bức tranh lớn.

Andrew: Và làm thế nào bạn có thể giữ được khối lượng như vậy trong đầu? Điều này đạt được nhờ có nhiều kinh nghiệm hơn hay không? Kinh nghiệm như vậy đến từ đâu?

vách đá: Chà, tôi đã không có được trải nghiệm của mình một cách dễ dàng nhất. Tôi đã lập trình trong Assembly vào thời mà bạn có thể hiểu từng lệnh một. Nghe có vẻ ngu ngốc nhưng kể từ đó tập lệnh Z80 luôn đọng lại trong đầu tôi, trong trí nhớ của tôi. Tôi không nhớ tên mọi người trong vòng một phút nói chuyện, nhưng tôi nhớ mã được viết cách đây 40 năm. Buồn cười thật, trông giống như một hội chứng vậy"nhà khoa học ngốc nghếch'.

Đào tạo tối ưu hóa cấp độ thấp

Andrew: Có cách nào dễ dàng hơn để vào được không?

vách đá: Có và không. Phần cứng mà tất cả chúng ta sử dụng không thay đổi nhiều theo thời gian. Mọi người đều sử dụng x86, ngoại trừ điện thoại thông minh Arm. Nếu bạn không thực hiện một số kiểu nhúng mạnh mẽ nào đó, thì bạn cũng đang làm điều tương tự. Được rồi, tiếp theo. Các hướng dẫn cũng không thay đổi trong nhiều thế kỷ. Bạn cần phải viết một cái gì đó trong Assembly. Không nhiều, nhưng đủ để bắt đầu hiểu. Bạn đang mỉm cười, nhưng tôi đang nói hoàn toàn nghiêm túc. Bạn cần hiểu sự tương ứng giữa ngôn ngữ và phần cứng. Sau đó, bạn cần phải viết một chút và tạo một trình biên dịch đồ chơi nhỏ cho ngôn ngữ đồ chơi nhỏ. Giống như đồ chơi có nghĩa là nó cần được làm trong một khoảng thời gian hợp lý. Nó có thể cực kỳ đơn giản nhưng nó phải tạo ra các hướng dẫn. Hành động tạo lệnh sẽ giúp bạn hiểu mô hình chi phí làm cầu nối giữa mã cấp cao mà mọi người viết và mã máy chạy trên phần cứng. Sự tương ứng này sẽ được ghi vào não tại thời điểm trình biên dịch được viết. Ngay cả trình biên dịch đơn giản nhất. Sau đó, bạn có thể bắt đầu xem xét Java và thực tế là vực thẳm ngữ nghĩa của nó sâu hơn nhiều và việc xây dựng những cầu nối vượt qua nó sẽ khó khăn hơn nhiều. Trong Java, việc hiểu cây cầu của chúng ta tốt hay xấu sẽ khó hơn nhiều, điều gì sẽ khiến nó bị hỏng và điều gì sẽ không. Nhưng bạn cần một số điểm bắt đầu mà bạn nhìn vào mã và hiểu: "vâng, getter này phải được nội tuyến mọi lúc." Và sau đó, hóa ra đôi khi điều này xảy ra, ngoại trừ trường hợp khi phương thức trở nên quá lớn và JIT bắt đầu nội tuyến hóa mọi thứ. Hiệu suất của những nơi như vậy có thể được dự đoán ngay lập tức. Thông thường các getters hoạt động tốt, nhưng sau đó bạn nhìn vào các vòng lặp nóng lớn và nhận ra rằng có một số lệnh gọi hàm trôi nổi ở đó mà không biết chúng đang làm gì. Đây là vấn đề với việc sử dụng rộng rãi các getter, lý do tại sao chúng không được nội tuyến là do không rõ liệu chúng có phải là getter hay không. Nếu bạn có một cơ sở mã siêu nhỏ, bạn có thể chỉ cần nhớ nó và nói: đây là một getter, và đây là một setter. Trong một cơ sở mã lớn, mỗi chức năng có lịch sử riêng của nó, mà nói chung là không ai biết đến. Trình lược tả nói rằng chúng ta đã mất 24% thời gian trên một số vòng lặp và để hiểu vòng lặp này đang làm gì, chúng ta cần xem xét từng chức năng bên trong. Không thể hiểu được điều này nếu không nghiên cứu chức năng, và điều này làm chậm nghiêm trọng quá trình hiểu biết. Đó là lý do tại sao tôi không sử dụng getters và setters, tôi đã đạt đến một cấp độ mới!
Lấy mô hình chi phí ở đâu? Tất nhiên là bạn có thể đọc được điều gì đó... Nhưng tôi nghĩ cách tốt nhất là hành động. Tạo một trình biên dịch nhỏ sẽ là cách tốt nhất để hiểu mô hình chi phí và đưa nó vào đầu bạn. Một trình biên dịch nhỏ phù hợp để lập trình lò vi sóng là một nhiệm vụ dành cho người mới bắt đầu. Ý tôi là, nếu bạn đã có kỹ năng lập trình thì thế là đủ. Tất cả những việc này như phân tích cú pháp một chuỗi mà bạn có dưới dạng một loại biểu thức đại số nào đó, trích xuất các hướng dẫn cho các phép toán từ đó theo đúng thứ tự, lấy các giá trị chính xác từ các thanh ghi - tất cả điều này được thực hiện cùng một lúc. Và khi bạn làm điều đó, nó sẽ in sâu vào não bạn. Tôi nghĩ mọi người đều biết trình biên dịch làm gì. Và điều này sẽ cung cấp sự hiểu biết về mô hình chi phí.

Ví dụ thực tế về cải thiện hiệu suất

Andrew: Bạn cần chú ý điều gì khác khi làm việc về năng suất?

vách đá: Cấu trúc dữ liệu. Nhân tiện, vâng, đã lâu rồi tôi không dạy những lớp này... Trường tên lửa. Việc đó rất vui nhưng đòi hỏi rất nhiều nỗ lực và tôi cũng có cuộc sống! ĐƯỢC RỒI. Vì vậy, tại một trong những lớp học lớn và thú vị, “Hiệu suất của bạn đi đến đâu”, tôi đã đưa ra một ví dụ cho học sinh: hai gigabyte dữ liệu fintech được đọc từ tệp CSV và sau đó họ phải tính số lượng sản phẩm đã bán . Dữ liệu thị trường đánh dấu thường xuyên. Các gói UDP được chuyển đổi sang định dạng văn bản từ những năm 70. Chicago Mercantile Exchange - tất cả những thứ như bơ, ngô, đậu nành, những thứ tương tự. Cần phải đếm các sản phẩm này, số lượng giao dịch, khối lượng chuyển động trung bình của tiền và hàng hóa, v.v. Đây là phép toán giao dịch khá đơn giản: tìm mã sản phẩm (có 1-2 ký tự trong bảng băm), lấy số tiền, thêm nó vào một trong các bộ giao dịch, thêm khối lượng, thêm giá trị và một vài thứ khác. Toán học rất đơn giản. Việc triển khai đồ chơi rất đơn giản: mọi thứ đều nằm trong một tệp, tôi đọc tệp và duyệt qua nó, chia các bản ghi riêng lẻ thành các chuỗi Java, tìm kiếm những thứ cần thiết trong đó và cộng chúng lại theo phép toán được mô tả ở trên. Và nó hoạt động ở tốc độ thấp.

Với cách tiếp cận này, rõ ràng điều gì đang diễn ra và tính toán song song sẽ không giúp ích được gì, phải không? Hóa ra có thể đạt được hiệu suất tăng gấp năm lần chỉ bằng cách chọn cấu trúc dữ liệu phù hợp. Và điều này gây ngạc nhiên ngay cả những lập trình viên giàu kinh nghiệm! Trong trường hợp cụ thể của tôi, mẹo là bạn không nên phân bổ bộ nhớ trong vòng lặp nóng. Chà, đây không phải là toàn bộ sự thật, nhưng nói chung - bạn không nên đánh dấu “một lần trong X” khi X đủ lớn. Khi X là hai gigabyte, bạn không nên phân bổ bất cứ thứ gì “một lần cho mỗi chữ cái”, “một lần trên mỗi dòng” hoặc “một lần cho mỗi trường”, bất cứ thứ gì tương tự. Đây là nơi dành thời gian. Làm thế nào điều này thậm chí hoạt động? Hãy tưởng tượng tôi đang thực hiện một cuộc gọi String.split() hoặc BufferedReader.readLine(). Readline tạo một chuỗi từ một tập hợp byte đi qua mạng, một lần cho mỗi dòng, cho mỗi dòng trong số hàng trăm triệu dòng. Tôi lấy dòng này, phân tích nó và vứt nó đi. Tại sao tôi lại vứt nó đi - à, tôi đã xử lý xong rồi, thế thôi. Vì vậy, đối với mỗi byte được đọc từ 2.7G này, hai ký tự sẽ được viết trong dòng, tức là đã là 5.4G và tôi không cần chúng cho bất kỳ mục đích nào nữa nên chúng sẽ bị loại bỏ. Nếu bạn nhìn vào băng thông bộ nhớ, chúng tôi tải 2.7G đi qua bộ nhớ và bus bộ nhớ trong bộ xử lý, sau đó gấp đôi lượng được gửi đến dòng nằm trong bộ nhớ và tất cả điều này sẽ bị hỏng khi mỗi dòng mới được tạo. Nhưng tôi cần phải đọc nó, phần cứng đọc nó, ngay cả khi sau này mọi thứ đều hỏng hóc. Và tôi phải ghi lại vì tôi đã tạo một dòng và bộ đệm đã đầy - bộ đệm không chứa được 2.7G. Vì vậy, với mỗi byte tôi đọc, tôi đọc thêm hai byte và ghi thêm hai byte, và cuối cùng chúng có tỷ lệ 4:1 - theo tỷ lệ này, chúng ta đang lãng phí băng thông bộ nhớ. Và sau đó hóa ra là nếu tôi làm String.split() – đây không phải là lần cuối cùng tôi làm việc này, bên trong có thể còn 6-7 ô nữa. Vì vậy, mã cổ điển là đọc CSV rồi phân tích chuỗi dẫn đến lãng phí băng thông bộ nhớ khoảng 14:1 so với mức bạn thực sự muốn có. Nếu bạn loại bỏ những lựa chọn này, bạn có thể tăng tốc gấp năm lần.

Và nó không khó lắm. Nếu bạn nhìn mã từ góc độ phù hợp, mọi chuyện sẽ trở nên khá đơn giản khi bạn nhận ra vấn đề. Bạn không nên ngừng phân bổ bộ nhớ hoàn toàn: vấn đề duy nhất là bạn phân bổ thứ gì đó và nó ngay lập tức chết, và trong quá trình đó, nó sẽ đốt cháy một tài nguyên quan trọng, trong trường hợp này là băng thông bộ nhớ. Và tất cả điều này dẫn đến giảm năng suất. Trên x86, bạn thường cần tích cực ghi các chu kỳ xử lý, nhưng ở đây bạn đã đốt hết bộ nhớ sớm hơn nhiều. Giải pháp là giảm lượng xả. 
Phần khác của vấn đề là nếu bạn chạy trình lược tả khi dải bộ nhớ hết, ngay khi điều đó xảy ra, bạn thường đợi bộ đệm quay trở lại vì nó chứa đầy rác mà bạn vừa tạo ra, tất cả những dòng đó. Vì vậy, mọi thao tác tải hoặc lưu trữ đều trở nên chậm, vì dẫn đến cache miss - toàn bộ cache đã trở nên chậm, chờ rác thải ra ngoài. Do đó, trình lược tả sẽ chỉ hiển thị tiếng ồn ngẫu nhiên ấm áp trong toàn bộ vòng lặp - sẽ không có lệnh hoặc vị trí nóng riêng biệt trong mã. Chỉ có tiếng ồn. Và nếu bạn nhìn vào các chu trình GC, chúng đều thuộc Thế hệ trẻ và siêu nhanh - tối đa micro giây hoặc mili giây. Rốt cuộc, tất cả ký ức này sẽ chết ngay lập tức. Bạn phân bổ hàng tỷ gigabyte, và anh ta cắt chúng, cắt chúng, cắt chúng lại. Tất cả điều này xảy ra rất nhanh chóng. Hóa ra là có những chu trình GC rẻ tiền, tiếng ồn ấm trong toàn bộ chu trình, nhưng chúng tôi muốn tăng tốc gấp 5 lần. Tại thời điểm này, một cái gì đó sẽ đóng lại trong đầu bạn và phát ra âm thanh: "tại sao lại thế này ?!" Tràn dải bộ nhớ không được hiển thị trong trình gỡ lỗi cổ điển; bạn cần chạy trình gỡ lỗi bộ đếm hiệu suất phần cứng và tự mình xem nó một cách trực tiếp. Nhưng điều này không thể bị nghi ngờ trực tiếp từ ba triệu chứng này. Triệu chứng thứ ba là khi bạn nhìn vào những gì bạn đánh dấu, hãy hỏi người lập hồ sơ và anh ta trả lời: “Bạn đã tạo một tỷ hàng, nhưng GC hoạt động miễn phí”. Ngay khi điều này xảy ra, bạn nhận ra rằng mình đã tạo ra quá nhiều đồ vật và đốt cháy toàn bộ làn nhớ. Có một cách để tìm ra điều này, nhưng nó không rõ ràng. 

Vấn đề nằm ở cấu trúc dữ liệu: cấu trúc trần trụi bên dưới mọi thứ xảy ra, nó quá lớn, 2.7G trên đĩa, vì vậy việc tạo một bản sao của thứ này là điều không mong muốn - bạn muốn tải nó từ bộ đệm byte mạng ngay lập tức vào các thanh ghi để không đọc-ghi qua lại dòng năm lần. Thật không may, theo mặc định, Java không cung cấp cho bạn một thư viện như vậy như một phần của JDK. Nhưng điều này thật tầm thường, phải không? Về cơ bản, đây là 5-10 dòng mã sẽ được sử dụng để triển khai trình tải chuỗi đệm của riêng bạn, trình tải này lặp lại hành vi của lớp chuỗi, đồng thời đóng vai trò là trình bao bọc xung quanh bộ đệm byte bên dưới. Kết quả là, hóa ra là bạn đang làm việc gần như với các chuỗi, nhưng trên thực tế, các con trỏ tới bộ đệm đang di chuyển đến đó và các byte thô không được sao chép ở bất kỳ đâu và do đó, các bộ đệm giống nhau được sử dụng lại nhiều lần và hệ điều hành rất sẵn lòng đảm nhận những công việc mà nó được thiết kế, chẳng hạn như bộ đệm đôi ẩn của các bộ đệm byte này và bạn không còn phải nghiền ngẫm luồng dữ liệu không cần thiết vô tận nữa. Nhân tiện, bạn có hiểu rằng khi làm việc với GC, có đảm bảo rằng mỗi lần cấp phát bộ nhớ sẽ không hiển thị cho bộ xử lý sau chu kỳ GC cuối cùng không? Do đó, tất cả những thứ này không thể có trong bộ đệm và sau đó xảy ra lỗi được đảm bảo 100%. Khi làm việc với một con trỏ, trên x86, việc trừ một thanh ghi khỏi bộ nhớ mất 1-2 chu kỳ xung nhịp và ngay khi điều này xảy ra, bạn phải trả, trả, trả, vì bộ nhớ đã bật hoàn toàn NĂM bộ nhớ đệm – và đây là chi phí phân bổ bộ nhớ. Giá trị thực.

Nói cách khác, cấu trúc dữ liệu là thứ khó thay đổi nhất. Và một khi bạn nhận ra rằng mình đã chọn sai cấu trúc dữ liệu sẽ làm giảm hiệu suất sau này, thì thường có rất nhiều việc phải làm, nhưng nếu bạn không làm vậy, mọi thứ sẽ trở nên tồi tệ hơn. Trước hết, bạn cần nghĩ về cấu trúc dữ liệu, điều này rất quan trọng. Chi phí chính ở đây rơi vào các cấu trúc dữ liệu béo, đang bắt đầu được sử dụng theo kiểu “Tôi đã sao chép cấu trúc dữ liệu X thành cấu trúc dữ liệu Y vì tôi thích hình dạng của Y hơn”. Nhưng thao tác sao chép (có vẻ rẻ) thực sự lãng phí băng thông bộ nhớ và đó là nơi chôn vùi tất cả thời gian thực hiện lãng phí. Nếu tôi có một chuỗi JSON khổng lồ và tôi muốn biến nó thành một cây DOM có cấu trúc của POJO hoặc thứ gì đó, hoạt động phân tích chuỗi đó và xây dựng POJO, sau đó truy cập lại POJO sau đó sẽ dẫn đến chi phí không cần thiết - đó là không rẻ. Ngoại trừ trường hợp bạn chạy quanh POJO thường xuyên hơn nhiều so với chạy quanh một chuỗi. Thay vào đó, bạn có thể cố gắng giải mã chuỗi và chỉ trích xuất những gì bạn cần từ đó mà không cần biến nó thành bất kỳ POJO nào. Nếu tất cả điều này xảy ra trên một đường dẫn yêu cầu hiệu suất tối đa, không có POJO nào cho bạn, bạn cần phải đào sâu trực tiếp vào đường dẫn đó bằng cách nào đó.

Tại sao tạo ngôn ngữ lập trình của riêng bạn

Andrew: Bạn nói rằng để hiểu mô hình chi phí, bạn cần phải viết ngôn ngữ nhỏ của riêng mình...

vách đá: Không phải là ngôn ngữ, mà là trình biên dịch. Ngôn ngữ và trình biên dịch là hai thứ khác nhau. Sự khác biệt quan trọng nhất là trong đầu của bạn. 

Andrew: Nhân tiện, theo như tôi biết, bạn đang thử nghiệm việc tạo ngôn ngữ của riêng mình. Để làm gì?

vách đá: Bởi vì tôi có thể! Tôi đã nghỉ hưu nên đây là sở thích của tôi. Tôi đã thực hiện ngôn ngữ của người khác suốt cuộc đời mình. Tôi cũng đã làm việc rất nhiều về phong cách viết mã của mình. Và cũng bởi vì tôi thấy có vấn đề ở các ngôn ngữ khác. Tôi thấy rằng có nhiều cách tốt hơn để làm những việc quen thuộc. Và tôi sẽ sử dụng chúng. Tôi cảm thấy mệt mỏi khi phải nhìn thấy những vấn đề của bản thân mình, trong Java, trong Python, trong bất kỳ ngôn ngữ nào khác. Bây giờ tôi viết bằng React Native, JavaScript và Elm như một sở thích không phải về việc nghỉ hưu mà là về công việc tích cực. Tôi cũng viết bằng Python và rất có thể sẽ tiếp tục nghiên cứu máy học cho các chương trình phụ trợ Java. Có rất nhiều ngôn ngữ phổ biến và chúng đều có những tính năng thú vị. Mọi người đều tốt theo cách riêng của họ và bạn có thể cố gắng kết hợp tất cả những đặc điểm này lại với nhau. Vì vậy, tôi đang nghiên cứu những thứ mà tôi quan tâm, hành vi của ngôn ngữ, cố gắng tìm ra ngữ nghĩa hợp lý. Và cho đến nay tôi đã thành công! Hiện tại, tôi đang gặp khó khăn với ngữ nghĩa bộ nhớ, vì tôi muốn có nó giống như trong C và Java, đồng thời có được một mô hình bộ nhớ và ngữ nghĩa bộ nhớ mạnh mẽ để tải và lưu trữ. Đồng thời có khả năng suy luận kiểu tự động như trong Haskell. Ở đây, tôi đang cố gắng kết hợp suy luận kiểu giống Haskell với hoạt động của bộ nhớ trong cả C và Java. Ví dụ, đây là những gì tôi đã làm trong 2-3 tháng qua.

Andrew: Nếu bạn xây dựng một ngôn ngữ có những khía cạnh tốt hơn từ các ngôn ngữ khác, bạn có nghĩ rằng ai đó sẽ làm điều ngược lại: lấy ý tưởng của bạn và sử dụng chúng không?

vách đá: Đây chính xác là cách các ngôn ngữ mới xuất hiện! Tại sao Java giống với C? Bởi vì C có một cú pháp tốt mà mọi người đều hiểu và Java lấy cảm hứng từ cú pháp này, thêm tính an toàn về kiểu, kiểm tra giới hạn mảng, GC và họ cũng cải thiện một số thứ từ C. Họ đã thêm vào của riêng mình. Nhưng họ đã được truyền cảm hứng khá nhiều, phải không? Mọi người đều đứng trên vai những người khổng lồ đi trước bạn - đó là cách đạt được sự tiến bộ.

Andrew: Theo tôi hiểu, ngôn ngữ của bạn sẽ được lưu trữ an toàn trong bộ nhớ. Bạn đã nghĩ đến việc triển khai một cái gì đó giống như công cụ kiểm tra khoản vay từ Rust chưa? Bạn đã nhìn anh ấy chưa, bạn nghĩ gì về anh ấy?

vách đá: Chà, tôi đã viết C từ lâu rồi, với tất cả malloc và miễn phí này, đồng thời quản lý vòng đời theo cách thủ công. Bạn biết đấy, 90-95% thời gian sống được điều khiển thủ công đều có cấu trúc giống nhau. Và sẽ rất đau đớn khi làm điều đó bằng tay. Tôi muốn trình biên dịch chỉ cho bạn biết điều gì đang xảy ra ở đó và những gì bạn đã đạt được bằng hành động của mình. Đối với một số thứ, người kiểm tra khoản vay thực hiện việc này ngay lập tức. Và nó sẽ tự động hiển thị thông tin, hiểu mọi thứ và thậm chí không tạo gánh nặng cho tôi khi trình bày sự hiểu biết này. Ít nhất nó phải thực hiện phân tích thoát cục bộ và chỉ khi thất bại thì nó mới cần thêm các chú thích kiểu sẽ mô tả thời gian tồn tại - và sơ đồ như vậy phức tạp hơn nhiều so với trình kiểm tra mượn hoặc thực sự là bất kỳ trình kiểm tra bộ nhớ hiện có nào. Sự lựa chọn giữa “mọi thứ đều ổn” và “Tôi không hiểu gì cả” - không, phải có thứ gì đó tốt hơn. 
Vì vậy, là một người đã viết nhiều mã bằng C, tôi nghĩ rằng việc hỗ trợ kiểm soát vòng đời tự động là điều quan trọng nhất. Tôi cũng chán ngấy việc Java sử dụng bao nhiêu bộ nhớ và phàn nàn chính là GC. Khi bạn cấp phát bộ nhớ trong Java, bạn sẽ không lấy lại được bộ nhớ cục bộ ở chu kỳ GC cuối cùng. Điều này không xảy ra ở các ngôn ngữ có khả năng quản lý bộ nhớ chính xác hơn. Nếu bạn gọi malloc, bạn sẽ nhận được ngay bộ nhớ thường được sử dụng. Thông thường bạn làm một số việc tạm thời với bộ nhớ và ngay lập tức trả nó trở lại. Và nó ngay lập tức quay trở lại nhóm malloc, và chu kỳ malloc tiếp theo lại kéo nó ra. Do đó, mức sử dụng bộ nhớ thực tế sẽ giảm xuống đối với tập hợp các đối tượng sống tại một thời điểm nhất định, cộng với rò rỉ. Và nếu mọi thứ không bị rò rỉ theo cách hoàn toàn không đứng đắn, thì phần lớn bộ nhớ sẽ nằm trong bộ đệm và bộ xử lý, và nó sẽ hoạt động nhanh chóng. Nhưng đòi hỏi nhiều sự quản lý bộ nhớ thủ công với malloc và free được gọi theo đúng thứ tự, đúng chỗ. Rust có thể tự mình xử lý việc này một cách hợp lý và trong nhiều trường hợp thậm chí còn mang lại hiệu suất tốt hơn vì mức tiêu thụ bộ nhớ được thu hẹp xuống chỉ còn tính toán hiện tại - trái ngược với việc chờ chu kỳ GC tiếp theo giải phóng bộ nhớ. Kết quả là chúng tôi đã có được một cách rất thú vị để cải thiện hiệu suất. Và khá mạnh mẽ - ý tôi là, tôi đã làm những việc như vậy khi xử lý dữ liệu cho fintech và điều này cho phép tôi tăng tốc khoảng năm lần. Đó là một sự thúc đẩy khá lớn, đặc biệt là trong một thế giới mà bộ xử lý không thể nhanh hơn và chúng ta vẫn đang chờ đợi những cải tiến.

Sự nghiệp kỹ sư hiệu suất

Andrew: Tôi cũng muốn hỏi xung quanh về nghề nghiệp nói chung. Bạn đã trở nên nổi tiếng nhờ công việc JIT của mình tại HotSpot và sau đó chuyển đến Azul, cũng là một công ty JVM. Nhưng chúng tôi đã làm việc nhiều hơn về phần cứng hơn là phần mềm. Và sau đó họ đột ngột chuyển sang Dữ liệu lớn và Học máy, rồi chuyển sang phát hiện gian lận. Làm sao chuyện này lại xảy ra? Đây là những lĩnh vực phát triển rất khác nhau.

vách đá: Tôi đã lập trình được một thời gian khá dài và đã tham gia rất nhiều lớp học khác nhau. Và khi mọi người nói: “ồ, bạn là người đã làm JIT cho Java!”, điều đó luôn buồn cười. Nhưng trước đó, tôi đang nghiên cứu một bản sao của PostScript - ngôn ngữ mà Apple từng sử dụng cho máy in laser của mình. Và trước đó tôi đã triển khai ngôn ngữ Forth. Tôi nghĩ chủ đề chung của tôi là phát triển công cụ. Trong suốt cuộc đời mình, tôi đã tạo ra những công cụ để người khác viết ra những chương trình thú vị của họ. Nhưng tôi cũng tham gia vào việc phát triển hệ điều hành, trình điều khiển, trình gỡ lỗi cấp kernel, ngôn ngữ để phát triển hệ điều hành, những công việc ban đầu tưởng chừng như tầm thường nhưng theo thời gian ngày càng trở nên phức tạp hơn. Nhưng chủ đề chính vẫn là phát triển các công cụ. Một phần lớn cuộc đời tôi trôi qua giữa Azul và Sun, và đó là về Java. Nhưng khi tôi tham gia vào Dữ liệu lớn và Học máy, tôi lại đội chiếc mũ ưa thích của mình lên và nói, “Ồ, bây giờ chúng ta có một vấn đề không hề nhỏ, và có rất nhiều điều thú vị đang diễn ra và mọi người đang làm những việc đó.” Đây là một con đường phát triển tuyệt vời để đi.

Vâng, tôi thực sự yêu thích điện toán phân tán. Công việc đầu tiên của tôi là sinh viên lớp C trong một dự án quảng cáo. Đây là tính toán phân tán trên chip Zilog Z80 thu thập dữ liệu cho OCR tương tự, được tạo ra bởi một máy phân tích tương tự thực sự. Đó là một chủ đề thú vị và hoàn toàn điên rồ. Nhưng có vấn đề, một số phần không được nhận dạng chính xác nên bạn phải lấy một bức ảnh ra và đưa cho một người đã có thể đọc bằng mắt và ghi lại những gì nó nói, và do đó có những công việc liên quan đến dữ liệu, và những công việc này có ngôn ngữ riêng của họ. Có một phần phụ trợ xử lý tất cả những điều này - Z80 chạy song song với các thiết bị đầu cuối vt100 đang chạy - mỗi người một thiết bị và có một mô hình lập trình song song trên Z80. Một số phần bộ nhớ chung được chia sẻ bởi tất cả các Z80 trong cấu hình sao; Bảng nối đa năng cũng được chia sẻ và một nửa RAM được chia sẻ trong mạng, nửa còn lại là riêng tư hoặc dành cho mục đích khác. Một hệ thống phân tán song song phức tạp có ý nghĩa với bộ nhớ chia sẻ... bán chia sẻ. Đây là khi nào... Tôi thậm chí không thể nhớ được, đâu đó vào giữa những năm 80. Cách đây khá lâu rồi. 
Vâng, giả sử 30 năm là một khoảng thời gian khá dài, các vấn đề liên quan đến tính toán phân tán đã tồn tại từ khá lâu, con người từ lâu đã có chiến tranh với nó. Beowulf-cụm. Các cụm như vậy trông giống như... Ví dụ: có Ethernet và x86 nhanh của bạn được kết nối với Ethernet này và bây giờ bạn muốn có bộ nhớ dùng chung giả, vì khi đó không ai có thể thực hiện mã hóa máy tính phân tán, điều đó quá khó và do đó là bộ nhớ chia sẻ giả mạo với các trang bộ nhớ bảo vệ trên x86 và nếu bạn viết vào trang này thì chúng tôi đã thông báo với các bộ xử lý khác rằng nếu họ truy cập vào cùng một bộ nhớ dùng chung, nó sẽ cần được tải từ bạn và do đó, một cái gì đó giống như một giao thức để hỗ trợ sự kết hợp bộ đệm đã xuất hiện và phần mềm cho việc này. Khái niệm thú vị. Tất nhiên, vấn đề thực sự là một cái gì đó khác. Tất cả điều này đều hiệu quả, nhưng bạn nhanh chóng gặp vấn đề về hiệu suất, bởi vì không ai hiểu đủ tốt về các mô hình hiệu suất - có những kiểu truy cập bộ nhớ nào ở đó, làm cách nào để đảm bảo rằng các nút không ping nhau liên tục, v.v.

Điều tôi nghĩ ra trong H2O là chính các nhà phát triển là người chịu trách nhiệm xác định xem tính song song được ẩn ở đâu và ở đâu không. Tôi đã nghĩ ra một mô hình mã hóa giúp việc viết mã hiệu suất cao trở nên dễ dàng và đơn giản. Nhưng viết code chạy chậm thì khó, nhìn sẽ xấu. Bạn cần phải cố gắng viết mã chậm một cách nghiêm túc, bạn sẽ phải sử dụng các phương pháp không chuẩn. Mã phanh có thể nhìn thấy ngay từ cái nhìn đầu tiên. Kết quả là bạn thường viết mã chạy nhanh, nhưng bạn phải tìm ra những việc cần làm trong trường hợp bộ nhớ dùng chung. Tất cả điều này được gắn với các mảng lớn và hoạt động ở đó tương tự như các mảng lớn không thay đổi trong Java song song. Ý tôi là, hãy tưởng tượng rằng hai luồng ghi vào một mảng song song, một trong số chúng thắng, còn luồng kia thua và bạn không biết cái nào là cái nào. Nếu chúng không biến động thì thứ tự có thể là bất cứ thứ gì bạn muốn - và điều này thực sự hiệu quả. Mọi người thực sự quan tâm đến thứ tự hoạt động, họ đặt tính biến động vào đúng vị trí và họ mong đợi các vấn đề về hiệu suất liên quan đến bộ nhớ ở đúng vị trí. Nếu không, họ sẽ chỉ viết mã dưới dạng các vòng lặp từ 1 đến N, trong đó N là vài nghìn tỷ, với hy vọng rằng tất cả các trường hợp phức tạp sẽ tự động trở nên song song - và nó không hoạt động ở đó. Nhưng trong H2O đây không phải là Java hay Scala; bạn có thể coi nó là “Java trừ trừ” nếu muốn. Đây là một phong cách lập trình rất rõ ràng và tương tự như việc viết mã C hoặc Java đơn giản với các vòng lặp và mảng. Nhưng đồng thời, bộ nhớ có thể được xử lý tính bằng terabyte. Tôi vẫn sử dụng H2O. Tôi thỉnh thoảng sử dụng nó trong các dự án khác nhau - và nó vẫn là thứ nhanh nhất, nhanh hơn hàng chục lần so với các đối thủ cạnh tranh. Nếu bạn đang thực hiện Dữ liệu lớn với dữ liệu cột thì rất khó để đánh bại H2O.

Những thách thức kỹ thuật

Andrew: Thử thách lớn nhất trong toàn bộ sự nghiệp của bạn là gì?

vách đá: Chúng ta đang thảo luận về phần kỹ thuật hay phi kỹ thuật của vấn đề? Tôi có thể nói rằng thách thức lớn nhất không phải là vấn đề kỹ thuật. 
Đối với những thách thức kỹ thuật. Tôi chỉ đơn giản là đánh bại họ. Tôi thậm chí còn không biết điều lớn nhất là gì, nhưng có một số điều khá thú vị khiến tôi mất khá nhiều thời gian và đấu tranh tinh thần. Khi lên Sun, tôi chắc chắn rằng mình sẽ làm một trình biên dịch nhanh, và một loạt đàn anh đáp lại rằng tôi sẽ không bao giờ thành công. Nhưng tôi đã đi theo con đường này, viết một trình biên dịch xuống bộ cấp phát đăng ký và nó khá nhanh. Nó nhanh như C1 hiện đại, nhưng hồi đó bộ cấp phát chậm hơn nhiều và nhìn lại thì đó là một vấn đề về cấu trúc dữ liệu lớn. Tôi cần nó để viết một bộ cấp phát thanh ghi đồ họa và tôi không hiểu vấn đề nan giải giữa khả năng biểu đạt mã và tốc độ, vốn tồn tại trong thời đại đó và rất quan trọng. Hóa ra cấu trúc dữ liệu thường vượt quá kích thước bộ đệm trên x86 vào thời điểm đó và do đó, nếu ban đầu tôi giả định rằng bộ cấp phát đăng ký sẽ hoạt động 5-10% tổng thời gian jitter, thì thực tế hóa ra là như vậy. 50 phần trăm.

Theo thời gian, trình biên dịch trở nên sạch hơn và hiệu quả hơn, ngừng tạo ra mã khủng khiếp trong nhiều trường hợp hơn và hiệu suất ngày càng bắt đầu giống với những gì trình biên dịch C tạo ra. Tất nhiên, trừ khi bạn viết một số thứ tào lao mà ngay cả C cũng không tăng tốc . Nếu bạn viết mã như C, bạn sẽ nhận được hiệu suất như C trong nhiều trường hợp hơn. Và bạn càng đi xa, bạn càng thường xuyên nhận được mã tiệm cận với cấp C, bộ cấp phát đăng ký bắt đầu trông giống như một thứ gì đó hoàn chỉnh... bất kể mã của bạn chạy nhanh hay chậm. Tôi tiếp tục làm việc trên bộ phân bổ để làm cho nó đưa ra những lựa chọn tốt hơn. Anh ấy ngày càng trở nên chậm hơn, nhưng anh ấy ngày càng thể hiện hiệu suất tốt hơn trong những trường hợp mà không ai khác có thể đối phó được. Tôi có thể đi sâu vào một công cụ cấp phát đăng ký, chôn vùi một tháng làm việc ở đó và đột nhiên toàn bộ mã sẽ bắt đầu thực thi nhanh hơn 5%. Điều này xảy ra hết lần này đến lần khác và bộ phân bổ đăng ký đã trở thành một tác phẩm nghệ thuật - mọi người đều yêu thích hoặc ghét nó, và những người trong học viện đã đặt câu hỏi về chủ đề “tại sao mọi thứ lại được thực hiện theo cách này”, tại sao không quét dòng, và sự khác biệt là gì. Câu trả lời vẫn như cũ: một công cụ cấp phát dựa trên vẽ biểu đồ cộng với việc làm việc rất cẩn thận với mã đệm ngang bằng với vũ khí chiến thắng, sự kết hợp tốt nhất mà không ai có thể đánh bại. Và đây là một điều khá khó hiểu. Mọi thứ khác mà trình biên dịch thực hiện đều là những thứ được nghiên cứu khá kỹ lưỡng, mặc dù chúng cũng đã được nâng lên tầm nghệ thuật. Tôi luôn làm những việc được cho là biến trình biên dịch thành một tác phẩm nghệ thuật. Nhưng không có gì trong số này là bất thường - ngoại trừ bộ cấp phát đăng ký. Bí quyết là phải cẩn thận cắt giảm đang tải và nếu điều này xảy ra (tôi có thể giải thích chi tiết hơn nếu quan tâm), điều này có nghĩa là bạn có thể nội tuyến mạnh mẽ hơn mà không có nguy cơ bị vấp ngã trong lịch trình thực hiện. Vào thời đó, có rất nhiều trình biên dịch quy mô đầy đủ, treo đầy đồ trang sức và còi, có bộ phân bổ đăng ký, nhưng không ai khác có thể làm được điều đó.

Vấn đề là nếu bạn thêm các phương thức phải nội tuyến, tăng và tăng diện tích nội tuyến, tập hợp các giá trị được sử dụng sẽ ngay lập tức vượt xa số lượng thanh ghi và bạn phải cắt chúng đi. Mức độ quan trọng thường xuất hiện khi người phân bổ bỏ cuộc và một ứng cử viên sáng giá cho sự cố tràn có giá trị khác, bạn sẽ bán một số thứ nói chung là hoang dã. Giá trị của inline ở đây là bạn mất đi một phần overhead, overhead cho việc gọi và lưu, bạn có thể xem các giá trị bên trong và có thể tối ưu hóa chúng hơn nữa. Cái giá của việc nội tuyến là một số lượng lớn các giá trị trực tiếp được hình thành và nếu bộ cấp phát đăng ký của bạn đốt cháy nhiều hơn mức cần thiết, bạn sẽ thua ngay lập tức. Do đó, hầu hết các nhà phân bổ đều gặp phải một vấn đề: khi nội tuyến vượt qua một đường nhất định, mọi thứ trên thế giới bắt đầu bị cắt giảm và năng suất có thể bị xả xuống bồn cầu. Những người triển khai trình biên dịch sẽ thêm một số phương pháp phỏng đoán: ví dụ: dừng nội tuyến, bắt đầu với một số kích thước đủ lớn, vì việc phân bổ sẽ phá hỏng mọi thứ. Đây là cách hình thành một đường gấp khúc trong biểu đồ hiệu suất - bạn ở nội tuyến, nội tuyến, hiệu suất tăng dần - và sau đó bùng nổ! – nó rơi xuống như một con jack nhanh vì bạn xếp hàng quá nhiều. Đây là cách mọi thứ hoạt động trước khi Java ra đời. Java yêu cầu nội tuyến nhiều hơn, vì vậy tôi phải làm cho bộ cấp phát của mình linh hoạt hơn nhiều để nó cân bằng thay vì gặp sự cố và nếu bạn nội tuyến quá nhiều, nó sẽ bắt đầu tràn, nhưng sau đó thời điểm "không tràn nữa" vẫn đến. Đây là một quan sát thú vị và nó đến với tôi một cách bất ngờ, không rõ ràng, nhưng nó đã mang lại kết quả tốt. Tôi đã sử dụng nội tuyến tích cực và nó đã đưa tôi đến những nơi mà hiệu năng Java và C hoạt động song song. Chúng thực sự rất gần nhau - tôi có thể viết mã Java nhanh hơn đáng kể so với mã C và những thứ tương tự, nhưng nhìn chung, trong bức tranh toàn cảnh, chúng gần như có thể so sánh được. Tôi nghĩ một phần của công lao này là ở bộ cấp phát đăng ký, cho phép tôi nội tuyến một cách ngu ngốc nhất có thể. Tôi chỉ nội tuyến mọi thứ tôi nhìn thấy. Câu hỏi ở đây là liệu bộ cấp phát có hoạt động tốt hay không, liệu kết quả có phải là mã hoạt động thông minh hay không. Đây là một thách thức lớn: hiểu tất cả những điều này và làm cho nó thành công.

Một chút về phân bổ đăng ký và đa lõi

Vladimir: Các vấn đề như phân bổ đăng ký có vẻ như là một chủ đề muôn thuở, vô tận. Tôi tự hỏi liệu đã từng có một ý tưởng nào đó tưởng chừng đầy hứa hẹn nhưng lại thất bại trong thực tế?

vách đá: Chắc chắn! Phân bổ đăng ký là một lĩnh vực trong đó bạn cố gắng tìm một số phương pháp phỏng đoán để giải quyết vấn đề NP-đầy đủ. Và bạn không bao giờ có thể đạt được một giải pháp hoàn hảo, phải không? Điều này đơn giản là không thể. Hãy nhìn xem, phần tổng hợp Ahead of Time - nó cũng hoạt động kém. Cuộc trò chuyện ở đây là về một số trường hợp trung bình. Về hiệu suất điển hình, vì vậy bạn có thể đi và đo lường điều gì đó mà bạn cho là hiệu suất điển hình tốt - xét cho cùng, bạn đang nỗ lực cải thiện nó! Phân bổ đăng ký là một chủ đề về hiệu suất. Sau khi bạn có nguyên mẫu đầu tiên, nó sẽ hoạt động và sơn những gì cần thiết, công việc hiệu suất sẽ bắt đầu. Bạn cần học cách đo lường tốt. Tại sao nó lại quan trọng? Nếu bạn có dữ liệu rõ ràng, bạn có thể xem xét các lĩnh vực khác nhau và thấy: vâng, nó có ích ở đây, nhưng đó là nơi mọi thứ đã hỏng! Một số ý tưởng hay nảy ra, bạn thêm các phương pháp phỏng đoán mới và đột nhiên mọi thứ bắt đầu hoạt động tốt hơn một chút. Hoặc nó không bắt đầu. Tôi đã gặp rất nhiều trường hợp trong đó chúng tôi đang đấu tranh để đạt được hiệu suất XNUMX%, giúp tạo nên sự khác biệt cho sự phát triển của chúng tôi so với công ty cấp phát trước đó. Và lần nào nó cũng trông như thế này: chỗ nào đó bạn thắng, chỗ nào đó bạn thua. Nếu bạn có các công cụ phân tích hiệu suất tốt, bạn có thể tìm ra những ý tưởng đang thất bại và hiểu lý do tại sao chúng thất bại. Có lẽ nên để mọi thứ như hiện tại hoặc có thể thực hiện một cách tiếp cận nghiêm túc hơn để tinh chỉnh hoặc đi ra ngoài và sửa một thứ khác. Đó là cả đống thứ! Tôi đã thực hiện bản hack thú vị này, nhưng tôi cũng cần cái này, cái này và cái này - và sự kết hợp tổng thể của chúng mang lại một số cải tiến. Và những người cô đơn có thể thất bại. Đây là bản chất của việc thực hiện các bài toán NP-đầy đủ.

Vladimir: Người ta có cảm giác rằng những việc như vẽ tranh trong bộ cấp phát là một vấn đề đã được giải quyết. Chà, điều đó đã được quyết định cho bạn, xét theo những gì bạn đang nói, vậy thì nó có đáng không...

vách đá: Nó không được giải quyết như vậy. Chính bạn là người phải biến nó thành “giải quyết”. Có những vấn đề khó khăn và chúng cần được giải quyết. Một khi điều này được thực hiện, đã đến lúc làm việc hiệu quả. Bạn cần tiếp cận công việc này một cách phù hợp - thực hiện các điểm chuẩn, thu thập số liệu, giải thích các tình huống khi bạn quay lại phiên bản trước, bản hack cũ của bạn bắt đầu hoạt động trở lại (hoặc ngược lại, đã dừng). Và đừng bỏ cuộc cho đến khi bạn đạt được điều gì đó. Như tôi đã nói, nếu có những ý tưởng hay nhưng không hiệu quả, nhưng trong lĩnh vực phân bổ sổ đăng ký ý tưởng thì gần như vô tận. Ví dụ, bạn có thể đọc các ấn phẩm khoa học. Mặc dù bây giờ khu vực này đã bắt đầu di chuyển chậm hơn nhiều và trở nên rõ ràng hơn so với thời trẻ. Tuy nhiên, có vô số người đang làm việc trong lĩnh vực này và mọi ý tưởng của họ đều đáng để thử, tất cả họ đều đang chờ đợi. Và bạn không thể biết chúng tốt như thế nào trừ khi bạn thử chúng. Chúng tích hợp tốt như thế nào với mọi thứ khác trong bộ cấp phát của bạn, bởi vì một bộ cấp phát thực hiện rất nhiều việc và một số ý tưởng trong bộ cấp phát cụ thể của bạn sẽ không hoạt động, nhưng trong một bộ cấp phát khác thì chúng sẽ dễ dàng hoạt động. Cách chính để giành chiến thắng cho người phân bổ là kéo nội dung chậm ra ngoài đường dẫn chính và buộc nó phân chia dọc theo ranh giới của đường dẫn chậm. Vì vậy, nếu bạn muốn chạy GC, hãy đi theo con đường chậm, hủy tối ưu hóa, ném ngoại lệ, tất cả những thứ đó - bạn biết những thứ này tương đối hiếm. Và chúng thực sự rất hiếm, tôi đã kiểm tra. Bạn làm thêm công việc và nó loại bỏ rất nhiều hạn chế trên những con đường chậm này, nhưng điều đó không thực sự quan trọng vì chúng chậm và hiếm khi được di chuyển. Ví dụ: một con trỏ null - điều đó không bao giờ xảy ra, phải không? Bạn cần có một số đường dẫn cho những thứ khác nhau, nhưng chúng không nên can thiệp vào đường dẫn chính. 

Vladimir: Bạn nghĩ gì về đa lõi, khi có hàng nghìn lõi cùng một lúc? Đây có phải là một điều hữu ích?

vách đá: Sự thành công của GPU cho thấy nó khá hữu ích!

Vladimir: Họ khá chuyên biệt. Còn bộ xử lý có mục đích chung thì sao?

vách đá: Ồ, đó là mô hình kinh doanh của Azul. Câu trả lời đến vào thời đại mà mọi người thực sự yêu thích hiệu suất có thể dự đoán được. Hồi đó rất khó viết mã song song. Mô hình mã hóa H2O có khả năng mở rộng cao nhưng không phải là mô hình có mục đích chung. Có lẽ tổng quát hơn một chút so với khi sử dụng GPU. Chúng ta đang nói về sự phức tạp của việc phát triển một thứ như vậy hay sự phức tạp của việc sử dụng nó? Ví dụ, Azul đã dạy cho tôi một bài học thú vị, một bài học khá khó hiểu: bộ nhớ đệm nhỏ là điều bình thường. 

Thử thách lớn nhất trong cuộc đời

Vladimir: Còn những thách thức phi kỹ thuật thì sao?

vách đá: Thử thách lớn nhất là không... tử tế và tử tế với mọi người. Và kết quả là tôi liên tục rơi vào những tình huống cực kỳ mâu thuẫn. Những người mà tôi biết mọi thứ đang diễn ra không ổn nhưng không biết cách giải quyết những vấn đề đó và không thể giải quyết chúng. Nhiều vấn đề lâu dài, kéo dài hàng chục năm, đã nảy sinh theo cách này. Việc Java có trình biên dịch C1 và C2 là hệ quả trực tiếp của việc này. Việc không có trình biên dịch đa cấp trong Java trong mười năm liên tiếp cũng là một hậu quả trực tiếp. Rõ ràng là chúng ta cần một hệ thống như vậy nhưng không rõ tại sao nó lại không tồn tại. Tôi gặp vấn đề với một kỹ sư... hoặc một nhóm kỹ sư. Ngày xửa ngày xưa, khi bắt đầu làm việc ở Sun, tôi đã... Được rồi, không những thế, nhìn chung tôi luôn có quan điểm riêng của mình về mọi việc. Và tôi nghĩ đúng là bạn có thể lấy sự thật này của mình và nói thẳng ra. Đặc biệt là vì hầu hết thời gian tôi đều đúng một cách đáng kinh ngạc. Và nếu bạn không thích cách tiếp cận này... đặc biệt nếu bạn rõ ràng là sai và làm những điều vô nghĩa... Nói chung, rất ít người có thể chấp nhận được hình thức giao tiếp này. Mặc dù một số có thể, như tôi. Tôi đã xây dựng toàn bộ cuộc đời mình dựa trên nguyên tắc trọng dụng nhân tài. Nếu bạn chỉ cho tôi điều gì đó không ổn, tôi sẽ lập tức quay lại và nói: bạn nói vớ vẩn. Tất nhiên, đồng thời, tôi xin lỗi và tất cả những điều đó, tôi sẽ ghi nhận những giá trị xứng đáng, nếu có và thực hiện những hành động đúng đắn khác. Mặt khác, tôi đúng một cách đáng kinh ngạc về một tỷ lệ phần trăm lớn đến mức đáng kinh ngạc trong tổng thời gian. Và nó không có tác dụng tốt trong mối quan hệ với mọi người. Tôi không cố tỏ ra tử tế, nhưng tôi đang đặt câu hỏi một cách thẳng thắn. “Việc này sẽ không bao giờ có tác dụng, bởi vì một, hai và ba.” Và họ nói, "Ồ!" Có những hậu quả khác có lẽ tốt hơn nên bỏ qua: ví dụ, những hậu quả đã dẫn đến việc ly hôn với vợ tôi và mười năm trầm cảm sau đó.

Thử thách là cuộc đấu tranh với mọi người, với nhận thức của họ về những gì bạn có thể hoặc không thể làm, điều gì là quan trọng và điều gì không. Có rất nhiều thách thức về phong cách viết mã. Tôi vẫn viết rất nhiều mã, và vào thời điểm đó, tôi thậm chí phải làm việc chậm lại vì làm quá nhiều nhiệm vụ song song và thực hiện chúng kém, thay vì tập trung vào một nhiệm vụ. Nhìn lại, tôi đã viết một nửa mã cho lệnh Java JIT, lệnh C2. Lập trình viên nhanh nhất tiếp theo viết chậm một nửa, người tiếp theo chậm một nửa và đó là sự suy giảm theo cấp số nhân. Người thứ bảy ở hàng này rất, rất chậm - điều đó luôn xảy ra! Tôi đã chạm vào rất nhiều mã. Tôi xem ai đã viết gì, không có ngoại lệ, tôi nhìn chằm chằm vào mã của họ, xem xét từng mã và vẫn tiếp tục tự viết nhiều hơn bất kỳ ai trong số họ. Cách tiếp cận này không hiệu quả lắm với mọi người. Một số người không thích điều này. Và khi họ không thể giải quyết được thì đủ loại phàn nàn bắt đầu xảy ra. Ví dụ, tôi từng được yêu cầu ngừng viết mã vì tôi viết quá nhiều mã và điều đó gây nguy hiểm cho nhóm, và tất cả đối với tôi giống như một trò đùa: anh bạn, nếu phần còn lại của nhóm biến mất và tôi tiếp tục viết mã, bạn sẽ chỉ mất một nửa đội. Mặt khác, nếu tôi tiếp tục viết mã và bạn mất một nửa nhóm, điều đó có vẻ như là sự quản lý rất tệ. Tôi chưa bao giờ thực sự nghĩ về nó, chưa bao giờ nói về nó, nhưng nó vẫn ở đâu đó trong đầu tôi. Ý nghĩ đó quay cuồng trong đầu tôi: “Các bạn đang đùa tôi à?” Vì vậy, vấn đề lớn nhất là tôi và mối quan hệ của tôi với mọi người. Bây giờ tôi đã hiểu bản thân mình hơn rất nhiều, tôi từng là trưởng nhóm lập trình viên trong một thời gian dài, và bây giờ tôi trực tiếp nói với mọi người: bạn biết đấy, tôi là chính tôi, và bạn sẽ phải đối phó với tôi - tôi đứng vững có được không? đây? Và khi họ bắt đầu giải quyết nó, mọi thứ đều có kết quả. Thực ra tôi không xấu cũng không tốt, tôi không có ác ý hay khát vọng ích kỷ, đó chỉ là bản chất của tôi và tôi cần phải sống với nó bằng cách nào đó.

Andrew: Gần đây mọi người bắt đầu nói về khả năng tự nhận thức của người hướng nội và các kỹ năng mềm nói chung. bạn có thể nói gì về điều này?

vách đá: Vâng, đó là cái nhìn sâu sắc và bài học tôi rút ra được từ cuộc ly hôn với vợ. Điều tôi học được từ cuộc ly hôn là sự hiểu biết của chính mình. Đây là cách tôi bắt đầu hiểu người khác. Hiểu cách tương tác này hoạt động. Điều này dẫn đến những khám phá hết cái này đến cái khác. Tôi nhận thức được tôi là ai và tôi đại diện cho điều gì. Tôi đang làm gì: hoặc tôi đang bận tâm với nhiệm vụ, hoặc tôi đang trốn tránh xung đột, hoặc điều gì khác - và mức độ tự nhận thức này thực sự giúp tôi luôn kiểm soát được bản thân. Sau này mọi thứ trở nên dễ dàng hơn nhiều. Một điều mà tôi phát hiện ra không chỉ ở bản thân mình mà còn ở các lập trình viên khác là việc không thể diễn đạt thành lời những suy nghĩ khi bạn đang trong trạng thái căng thẳng về mặt cảm xúc. Ví dụ: bạn đang ngồi đó viết mã, trong trạng thái trôi chảy, sau đó họ chạy đến chỗ bạn và bắt đầu la hét trong cơn cuồng loạn rằng có thứ gì đó đã bị hỏng và bây giờ các biện pháp cực đoan sẽ được áp dụng để chống lại bạn. Và bạn không thể nói được lời nào vì bạn đang trong trạng thái căng thẳng về mặt cảm xúc. Kiến thức thu được cho phép bạn chuẩn bị cho thời điểm này, sống sót và chuyển sang kế hoạch rút lui, sau đó bạn có thể làm điều gì đó. Vì vậy, đúng vậy, khi bạn bắt đầu nhận ra mọi chuyện diễn ra như thế nào thì đó là một sự kiện thay đổi cuộc đời to lớn. 
Bản thân tôi không tìm được từ thích hợp nhưng tôi nhớ được chuỗi hành động. Vấn đề là phản ứng này mang tính chất vật lý cũng như bằng lời nói và bạn cần không gian. Không gian như vậy, theo nghĩa Thiền. Đây chính xác là điều cần được giải thích, và sau đó ngay lập tức bước sang một bên - hoàn toàn là bước đi về mặt vật lý. Khi tôi giữ im lặng bằng lời nói, tôi có thể xử lý tình huống một cách cảm xúc. Khi adrenaline đến não của bạn, chuyển bạn sang chế độ chiến đấu hoặc bỏ chạy, bạn không thể nói bất cứ điều gì nữa, không - bây giờ bạn là một tên ngốc, một kỹ sư đánh đòn, không có khả năng phản ứng tử tế hoặc thậm chí dừng cuộc tấn công, và kẻ tấn công được tự do để tấn công hết lần này đến lần khác. Trước tiên, bạn phải trở lại là chính mình, giành lại quyền kiểm soát, thoát khỏi chế độ “chiến đấu hoặc bỏ chạy”.

Và để làm được điều này, chúng ta cần không gian bằng lời nói. Chỉ cần không gian trống. Nếu bạn nói bất cứ điều gì, thì bạn có thể nói chính xác như vậy, sau đó đi và thực sự tìm “không gian” cho riêng mình: đi dạo trong công viên, nhốt mình trong phòng tắm - không thành vấn đề. Điều chính là tạm thời ngắt kết nối khỏi tình huống đó. Ngay sau khi bạn tắt ít nhất vài giây, khả năng kiểm soát sẽ quay trở lại, bạn bắt đầu suy nghĩ tỉnh táo. “Được rồi, tôi không phải loại ngốc, tôi không làm những điều ngu ngốc, tôi là một người khá hữu ích.” Khi bạn đã thuyết phục được bản thân, đã đến lúc chuyển sang giai đoạn tiếp theo: hiểu chuyện gì đã xảy ra. Bạn đã bị tấn công, cuộc tấn công đến từ nơi bạn không ngờ tới, đó là một cuộc phục kích hèn hạ, hèn hạ. Thật tệ. Bước tiếp theo là hiểu tại sao kẻ tấn công lại cần điều này. Thực sự, tại sao? Có lẽ vì bản thân anh đang tức giận chăng? Tại sao anh ấy lại nổi điên? Chẳng hạn, vì anh ta đã làm hỏng việc và không thể nhận trách nhiệm? Đây là cách để xử lý cẩn thận toàn bộ tình huống. Nhưng điều này đòi hỏi phải có chỗ cho sự vận động, không gian bằng lời nói. Bước đầu tiên là cắt đứt liên lạc bằng lời nói. Tránh thảo luận bằng lời nói. Hãy hủy bỏ nó, bỏ đi càng nhanh càng tốt. Nếu đó là cuộc trò chuyện qua điện thoại, hãy cúp máy - đây là kỹ năng tôi học được khi giao tiếp với vợ cũ. Nếu cuộc trò chuyện không đi đến đâu tốt đẹp, bạn chỉ cần nói "tạm biệt" và cúp máy. Từ bên kia điện thoại: “blah blah blah”, bạn trả lời: “vâng, tạm biệt!” và cúp máy. Bạn chỉ cần kết thúc cuộc trò chuyện. Năm phút sau, khi khả năng suy nghĩ nhạy bén trở lại với bạn, bạn đã nguội đi một chút, bạn có thể suy nghĩ về mọi thứ, chuyện gì đã xảy ra và chuyện gì sẽ xảy ra tiếp theo. Và bắt đầu hình thành một phản ứng chu đáo, thay vì chỉ phản ứng theo cảm xúc. Đối với tôi, bước đột phá trong khả năng tự nhận thức chính là việc tôi không thể nói được trong trường hợp căng thẳng về cảm xúc. Thoát khỏi trạng thái này, suy nghĩ và lên kế hoạch ứng phó và bù đắp cho các vấn đề - đây là những bước đi đúng đắn trong trường hợp bạn không thể nói được. Cách dễ nhất là chạy trốn khỏi tình huống căng thẳng cảm xúc bộc lộ và chỉ cần ngừng tham gia vào sự căng thẳng này. Sau đó bạn có thể suy nghĩ, khi bạn có thể suy nghĩ, bạn có thể nói, v.v.

Nhân tiện, tại tòa, luật sư đối lập cố gắng làm điều này với bạn - bây giờ thì đã rõ lý do. Bởi vì anh ta có khả năng trấn áp bạn đến mức bạn thậm chí không thể phát âm tên của mình chẳng hạn. Theo một nghĩa rất thực tế, bạn sẽ không thể nói được. Nếu điều này xảy ra với bạn, và nếu bạn biết rằng bạn sẽ thấy mình ở một nơi mà các cuộc đấu tranh bằng lời nói đang diễn ra ác liệt, ở một nơi như tòa án, thì bạn có thể đến với luật sư của mình. Luật sư sẽ đứng ra bảo vệ bạn và chấm dứt cuộc tấn công bằng lời nói, và sẽ thực hiện việc đó một cách hoàn toàn hợp pháp, và không gian Thiền đã mất sẽ trở lại với bạn. Ví dụ, tôi đã phải gọi điện cho gia đình vài lần, thẩm phán khá thân thiện về việc này, nhưng luật sư đối lập đã hét vào mặt tôi, tôi thậm chí không thể nói được một lời nào. Trong những trường hợp này, việc sử dụng một người hòa giải là phù hợp nhất với tôi. Người hòa giải ngăn chặn tất cả áp lực đang dồn lên bạn một cách liên tục, bạn tìm thấy không gian Thiền cần thiết và cùng với đó là khả năng nói chuyện trở lại. Đây là cả một lĩnh vực kiến ​​thức có rất nhiều điều để nghiên cứu, rất nhiều điều để khám phá bên trong bản thân bạn và tất cả những điều này biến thành những quyết định chiến lược cấp cao khác nhau đối với những người khác nhau. Một số người không gặp phải những vấn đề được mô tả ở trên; thông thường, những người bán hàng chuyên nghiệp không gặp phải những vấn đề này. Tất cả những người kiếm sống bằng ngôn từ - những ca sĩ, nhà thơ, nhà lãnh đạo tôn giáo và chính trị gia nổi tiếng, họ luôn có điều gì đó để nói. Họ không gặp phải những vấn đề như vậy, nhưng tôi thì có.

Andrew: Thật là không ngờ. Tuyệt vời, chúng ta đã nói chuyện rất nhiều và đã đến lúc kết thúc cuộc phỏng vấn này. Chúng tôi chắc chắn sẽ gặp nhau tại hội nghị và có thể tiếp tục cuộc đối thoại này. Hẹn gặp bạn ở Hydra!

Bạn có thể tiếp tục cuộc trò chuyện của mình với Cliff tại hội nghị Hydra 2019, sẽ được tổ chức vào ngày 11-12 tháng 2019 năm XNUMX tại St. Petersburg. Anh ấy sẽ đến với một bản báo cáo "Trải nghiệm bộ nhớ giao dịch phần cứng Azul". Có thể mua vé trên trang web chính thức.

Nguồn: www.habr.com

Thêm một lời nhận xét