Sử dụng CPU quá thấp của ứng dụng Java đa luồng trên Windows


18

Tôi đang làm việc trên một ứng dụng Java để giải quyết một lớp các vấn đề tối ưu hóa số - chính xác hơn là các vấn đề lập trình tuyến tính quy mô lớn. Một vấn đề duy nhất có thể được chia thành các bài toán con nhỏ hơn có thể giải quyết song song. Vì có nhiều bài toán con hơn lõi CPU, tôi sử dụng ExecutorService và định nghĩa mỗi bài toán con là một Callable được gửi đến ExecutorService. Việc giải một bài toán con đòi hỏi phải gọi một thư viện riêng - một trình giải lập trình tuyến tính trong trường hợp này.

Vấn đề

Tôi có thể chạy ứng dụng trên Unix và trên các hệ thống Windows với tối đa 44 lõi vật lý và bộ nhớ lên tới 256g, nhưng thời gian tính toán trên Windows là một mức độ lớn hơn so với Linux đối với các vấn đề lớn. Windows không chỉ đòi hỏi nhiều bộ nhớ hơn mà việc sử dụng CPU theo thời gian giảm từ 25% lúc đầu xuống còn 5% sau vài giờ. Dưới đây là ảnh chụp màn hình của trình quản lý tác vụ trong Windows:

Trình quản lý tác vụ sử dụng CPU

Quan sát

  • Thời gian giải pháp cho các trường hợp lớn của phạm vi vấn đề tổng thể từ vài giờ đến vài ngày và tiêu tốn tới 32g bộ nhớ (trên Unix). Thời gian giải pháp cho một bài toán con nằm trong phạm vi ms.
  • Tôi không gặp phải vấn đề này trên các vấn đề nhỏ mà chỉ mất vài phút để giải quyết.
  • Linux sử dụng cả hai ổ cắm bên ngoài, trong khi Windows yêu cầu tôi kích hoạt rõ ràng bộ nhớ xen kẽ trong BIOS để ứng dụng sử dụng cả hai lõi. Dù không, tôi không làm điều này không ảnh hưởng đến việc sử dụng CPU nói chung theo thời gian.
  • Khi tôi nhìn vào các luồng trong VisualVM, tất cả các luồng pool đang chạy, không có chuỗi nào đang chờ hoặc khác.
  • Theo VisualVM, 90% thời gian CPU dành cho một cuộc gọi chức năng gốc (giải quyết một chương trình tuyến tính nhỏ)
  • Bộ sưu tập rác không phải là một vấn đề vì ứng dụng không tạo và hủy tham chiếu rất nhiều đối tượng. Ngoài ra, hầu hết bộ nhớ dường như được phân bổ ngoài heap. 4g heap là đủ trên Linux và 8g trên Windows trong trường hợp lớn nhất.

Những gì tôi đã thử

  • tất cả các loại đối số JVM, XMS cao, metaspace cao, cờ UseNUMA, các GC khác.
  • các JVM khác nhau (Hotspot 8, 9, 10, 11).
  • các thư viện riêng khác nhau của các bộ giải lập trình tuyến tính khác nhau (CLP, Xpress, Cplex, Gurobi).

Câu hỏi

  • Điều gì thúc đẩy sự khác biệt về hiệu năng giữa Linux và Windows của một ứng dụng Java đa luồng lớn, sử dụng nhiều cuộc gọi gốc?
  • Có bất cứ điều gì mà tôi có thể thay đổi trong quá trình triển khai sẽ giúp Windows, chẳng hạn, tôi có nên tránh sử dụng ExecutorService nhận hàng ngàn Callables và thay vào đó làm gì không?

Bạn đã thử ForkJoinPoolthay vì ExecutorService? Mức sử dụng CPU 25% là rất thấp nếu vấn đề của bạn bị ràng buộc bởi CPU.
Karol Dowbecki

1
Vấn đề của bạn nghe có vẻ như là thứ gì đó sẽ đẩy CPU lên 100% và bạn vẫn ở mức 25%. Đối với một số vấn đề ForkJoinPoollà hiệu quả hơn so với lập kế hoạch thủ công.
Karol Dowbecki

2
Đi xe đạp qua các phiên bản Hotspot, bạn có chắc chắn rằng bạn đang sử dụng phiên bản "máy chủ" chứ không phải "máy khách" không? Việc sử dụng CPU của bạn trên Linux là gì? Ngoài ra, thời gian hoạt động của Windows trong vài ngày là rất ấn tượng! Bí mật của bạn là gì? : P
erickson

3
Có thể thử sử dụng Xperf để tạo FlameGraph . Điều này có thể cung cấp cho bạn cái nhìn sâu sắc về CPU đang làm gì (hy vọng cả chế độ người dùng và nhân), nhưng tôi chưa bao giờ làm điều đó trên Windows.
Karol Dowbecki

1
@Nils, cả hai lần chạy (unix / win) sử dụng cùng một giao diện để gọi thư viện gốc? Tôi hỏi, bởi vì nó trông giống như khác nhau. Giống như: win sử dụng jna, linux jni.
SR

Câu trả lời:


2

Đối với Windows, số lượng luồng trên mỗi quy trình bị giới hạn bởi không gian địa chỉ của quy trình (xem thêm Mark Russinovich - Đẩy các giới hạn của Windows: Quy trình và Chủ đề ). Hãy nghĩ rằng điều này gây ra tác dụng phụ khi gần đến giới hạn (làm chậm các chuyển đổi ngữ cảnh, phân mảnh ...). Đối với Windows, tôi sẽ cố gắng phân chia tải công việc cho một tập hợp các quy trình. Đối với một vấn đề tương tự mà tôi đã có từ nhiều năm trước, tôi đã triển khai một thư viện Java để thực hiện việc này thuận tiện hơn (Java 8), hãy xem nếu bạn thích: Thư viện để sinh ra các tác vụ trong một quy trình bên ngoài .


Điều này có vẻ rất thú vị! Tôi hơi do dự khi đi xa đến vậy (chưa) vì hai lý do: 1) sẽ có một chi phí hoạt động của việc tuần tự hóa và gửi các đối tượng qua các ổ cắm; 2) nếu tôi muốn tuần tự hóa mọi thứ, điều này bao gồm tất cả các phụ thuộc được liên kết trong một tác vụ - đó sẽ là một chút công việc để viết lại mã - dù sao, cảm ơn bạn vì (các) liên kết hữu ích.
Nils

Tôi hoàn toàn chia sẻ mối quan tâm của bạn và thiết kế lại mã sẽ là một số nỗ lực. Trong khi duyệt qua biểu đồ, bạn sẽ cần đưa ra một ngưỡng cho số lượng luồng khi đến lúc phân chia công việc thành một quy trình phụ mới. Để giải quyết vấn đề 2) hãy xem tệp ánh xạ bộ nhớ Java (java.nio.MappedByteBuffer), với điều đó bạn có thể chia sẻ dữ liệu giữa các quy trình một cách hiệu quả, ví dụ như dữ liệu biểu đồ của bạn. Chúc may mắn :)
geri

0

Âm thanh như cửa sổ đang lưu một số bộ nhớ vào pagefile, sau khi nó không được xử lý trong một thời gian và đó là lý do tại sao CPU bị tắc nghẽn bởi tốc độ Đĩa

Bạn có thể xác minh nó với Process explorer và kiểm tra dung lượng bộ nhớ được lưu trữ


Bạn nghĩ? Có đủ bộ nhớ trống. Tại sao Windows sẽ bắt đầu hoán đổi? Dù sao, cảm ơn.
Nils

Ít nhất trên các cửa sổ máy tính xách tay của tôi bị tráo đổi đôi khi các ứng dụng bị thu nhỏ, thậm chí có đủ bộ nhớ
Jew

0

Tôi nghĩ rằng sự khác biệt hiệu suất này là do cách hệ điều hành quản lý các luồng. JVM ẩn tất cả sự khác biệt của hệ điều hành. Có rất nhiều trang web mà bạn có thể đọc về nó, ví dụ như thế này . Nhưng nó không có nghĩa là sự khác biệt biến mất.

Tôi cho rằng bạn đang chạy trên Java 8+ JVM. Do thực tế này, tôi khuyên bạn nên thử sử dụng các tính năng lập trình luồng và chức năng. Lập trình hàm rất hữu ích khi bạn có nhiều vấn đề độc lập nhỏ và bạn muốn dễ dàng chuyển từ thực hiện tuần tự sang thực thi song song. Tin vui là bạn không phải xác định chính sách để xác định số lượng luồng bạn phải quản lý (như với ExecutorService). Ví dụ (lấy từ đây ):

package com.mkyong.java8;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class ParallelExample4 {

    public static void main(String[] args) {

        long count = Stream.iterate(0, n -> n + 1)
                .limit(1_000_000)
                //.parallel()   with this 23s, without this 1m 10s
                .filter(ParallelExample4::isPrime)
                .peek(x -> System.out.format("%s\t", x))
                .count();

        System.out.println("\nTotal: " + count);

    }

    public static boolean isPrime(int number) {
        if (number <= 1) return false;
        return !IntStream.rangeClosed(2, number / 2).anyMatch(i -> number % i == 0);
    }

}

Kết quả:

Đối với các luồng thông thường, phải mất 1 phút 10 giây. Đối với các luồng song song, phải mất 23 giây. PS đã thử nghiệm với i7-7700, RAM 16G, 10

Vì vậy, tôi khuyên bạn nên đọc về lập trình hàm, luồng, hàm lambda trong Java và thử thực hiện một số lượng thử nghiệm nhỏ với mã của bạn (được điều chỉnh để hoạt động trong ngữ cảnh mới này).


Tôi sử dụng các luồng trong các phần khác của phần mềm, nhưng trong trường hợp này, các tác vụ được tạo trong khi duyệt qua biểu đồ. Tôi sẽ không biết làm thế nào để bọc này bằng cách sử dụng các luồng.
Nils

Bạn có thể duyệt qua biểu đồ, xây dựng danh sách và sau đó sử dụng các luồng không?
xcesco

Các luồng song song chỉ là đường cú pháp cho một ForkJoinPool. Điều đó tôi đã thử (xem bình luận @KarolDowbecki ở trên).
Nils

0

Bạn vui lòng gửi số liệu thống kê hệ thống? Trình quản lý tác vụ đủ tốt để cung cấp một số đầu mối nếu đó là công cụ duy nhất có sẵn. Nó có thể dễ dàng biết liệu các nhiệm vụ của bạn có đang chờ IO hay không - nghe có vẻ là thủ phạm dựa trên những gì bạn mô tả. Nó có thể là do vấn đề quản lý bộ nhớ nhất định hoặc thư viện có thể ghi một số dữ liệu tạm thời vào đĩa, v.v.

Khi bạn nói 25% mức sử dụng CPU, bạn có nghĩa là chỉ có một vài lõi bận rộn làm việc cùng một lúc? (Có thể là tất cả các lõi hoạt động theo thời gian, nhưng không đồng thời.) Bạn có kiểm tra xem có bao nhiêu luồng (hoặc quy trình) thực sự được tạo trong hệ thống không? Là số luôn luôn lớn hơn số lượng lõi?

Nếu có đủ chủ đề, nhiều người trong số họ đang chờ đợi một cái gì đó? Nếu đúng, bạn có thể thử ngắt (hoặc đính kèm trình gỡ lỗi) để xem những gì họ đang chờ đợi.


Tôi đã thêm một ảnh chụp màn hình của trình quản lý tác vụ cho một thực thi là đại diện cho vấn đề này. Ứng dụng tự tạo ra nhiều luồng như có các lõi vật lý trên máy. Java đóng góp hơn 50 chủ đề cho con số đó. Như VisualVM đã nói, tất cả các luồng đều bận (màu xanh lá cây). Họ chỉ không đẩy CPU đến giới hạn trên Windows. Họ làm trên Linux.
Nils

@Nils Tôi nghi ngờ bạn không thực sự có tất cả các chủ đề bận rộn cùng một lúc, nhưng thực tế chỉ có 9 - 10 trong số đó. Chúng được lên lịch ngẫu nhiên trên tất cả các lõi, do đó bạn có mức sử dụng trung bình 9/44 = 20%. Bạn có thể sử dụng các luồng Java trực tiếp thay vì ExecutorService để thấy sự khác biệt không? Không khó để tạo ra 44 luồng và mỗi luồng lấy Runnable / Callable từ một nhóm tác vụ / hàng đợi. (Mặc dù VisualVM cho thấy tất cả các luồng Java đang bận, nhưng thực tế có thể là 44 luồng được lên lịch nhanh chóng để tất cả chúng có cơ hội chạy trong giai đoạn lấy mẫu của VisualVM.)
Xiao-Feng Li

Đó là một suy nghĩ và một cái gì đó mà tôi thực sự đã làm tại một số điểm. Trong triển khai của mình, tôi cũng đảm bảo rằng quyền truy cập gốc là cục bộ của từng luồng, nhưng điều này không tạo ra sự khác biệt nào cả.
Nils
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.