Làm thế nào xấu là nó gọi println () thường hơn là nối các chuỗi với nhau và gọi nó một lần?


23

Tôi biết đầu ra cho giao diện điều khiển là một hoạt động tốn kém. Vì lợi ích của khả năng đọc mã đôi khi, thật tốt khi gọi một hàm để xuất văn bản hai lần, thay vì có một chuỗi văn bản dài làm đối số.

Ví dụ, nó có hiệu quả thấp hơn bao nhiêu

System.out.println("Good morning.");
System.out.println("Please enter your name");

so với

System.out.println("Good morning.\nPlease enter your name");

Trong ví dụ, sự khác biệt chỉ là một cuộc gọi đến println()nhưng nếu nó nhiều hơn thì sao?

Trên một lưu ý liên quan, các câu lệnh liên quan đến in văn bản có thể trông lạ trong khi xem mã nguồn nếu văn bản cần in dài. Giả sử bản thân văn bản không thể được làm ngắn hơn, có thể làm gì? Đây có nên là một trường hợp mà nhiều println()cuộc gọi được thực hiện? Ai đó đã từng nói với tôi một dòng mã không nên nhiều hơn 80 ký tự (IIRC), vậy bạn sẽ làm gì với

System.out.println("Good morning everyone. I am here today to present you with a very, very lengthy sentence in order to prove a point about how it looks strange amongst other code.");

Điều này có đúng với các ngôn ngữ như C / C ++ không vì mỗi lần dữ liệu được ghi vào luồng đầu ra, một cuộc gọi hệ thống phải được thực hiện và quá trình phải chuyển sang chế độ kernel (rất tốn kém)?


Mặc dù đây là rất ít mã, tôi phải nói rằng tôi đã tự hỏi điều tương tự. Sẽ rất tốt để xác định câu trả lời cho điều này một lần và mãi mãi
Simon Forsberg

@ SimonAndréForsberg Tôi không chắc liệu nó có áp dụng được với Java hay không bởi vì nó chạy trên một máy ảo, nhưng trong các ngôn ngữ cấp thấp hơn như C / C ++, tôi sẽ tưởng tượng rằng nó sẽ tốn kém khi mỗi lần một thứ gì đó ghi vào luồng đầu ra, một cuộc gọi hệ thống phải được thực hiện.

Cũng có điều này để xem xét: stackoverflow.com/questions/21947452/ trên
hjk

1
Tôi phải nói rằng tôi không thấy vấn đề ở đây. Khi tương tác với người dùng qua thiết bị đầu cuối, tôi không thể tưởng tượng được bất kỳ vấn đề nào về hiệu suất vì thường không có nhiều thứ để in. Và các ứng dụng có GUI hoặc ứng dụng web nên ghi vào tệp nhật ký (thường sử dụng khung).
Andy

1
Nếu bạn đang nói buổi sáng tốt lành, bạn làm điều đó một hoặc hai lần một ngày. Tối ưu hóa không phải là một mối quan tâm. Nếu nó là một cái gì đó khác, bạn cần phải hồ sơ để biết nếu nó là một vấn đề. Mã tôi làm việc khi ghi nhật ký làm chậm mã thành không sử dụng được trừ khi bạn xây dựng bộ đệm nhiều dòng và kết xuất văn bản trong một cuộc gọi.
mattnz

Câu trả lời:


29

Có hai "lực lượng" ở đây, căng thẳng: Hiệu suất so với Khả năng đọc.

Trước tiên, hãy giải quyết vấn đề thứ ba, dài dòng:

System.out.println("Good morning everyone. I am here today to present you with a very, very lengthy sentence in order to prove a point about how it looks strange amongst other code.");

Cách tốt nhất để thực hiện điều này và giữ mức độ dễ đọc, là sử dụng nối chuỗi:

System.out.println("Good morning everyone. I am here today to present you "
                 + "with a very, very lengthy sentence in order to prove a "
                 + "point about how it looks strange amongst other code.");

Việc nối chuỗi liên tục sẽ xảy ra tại thời điểm biên dịch và hoàn toàn không ảnh hưởng đến hiệu suất. Các dòng có thể đọc được, và bạn có thể tiếp tục.

Bây giờ, về:

System.out.println("Good morning.");
System.out.println("Please enter your name");

so với

System.out.println("Good morning.\nPlease enter your name");

Tùy chọn thứ hai nhanh hơn đáng kể. Tôi sẽ đề nghị về 2X nhanh như vậy .... tại sao?

Bởi vì 90% (với biên độ sai số rộng) của tác phẩm không liên quan đến việc bỏ các ký tự vào đầu ra, nhưng là chi phí cần thiết để bảo đảm đầu ra để ghi vào nó.

Đồng bộ hóa

System.outlà một PrintStream. Tất cả các triển khai Java mà tôi biết, đồng bộ hóa nội bộ PrintStream: Xem mã trên GrepCode! .

Điều này có ý nghĩa gì với mã của bạn?

Điều đó có nghĩa là mỗi lần bạn gọi System.out.println(...)bạn đang đồng bộ hóa mô hình bộ nhớ, bạn đang kiểm tra và chờ khóa. Bất kỳ luồng nào khác gọi System.out cũng sẽ bị khóa.

Trong các ứng dụng đơn luồng, tác động của System.out.println()thường bị giới hạn bởi hiệu suất IO của hệ thống của bạn, bạn có thể ghi ra tệp nhanh như thế nào. Trong các ứng dụng đa luồng, việc khóa có thể là một vấn đề hơn so với IO.

Rửa

Mỗi println được tuôn ra . Điều này làm cho bộ đệm bị xóa và kích hoạt ghi mức Console cho bộ đệm. Lượng nỗ lực được thực hiện ở đây phụ thuộc vào việc thực hiện, nhưng, người ta thường hiểu rằng hiệu suất của việc xả chỉ là một phần nhỏ liên quan đến kích thước của bộ đệm được xả. Có một chi phí đáng kể liên quan đến việc xả, trong đó bộ đệm được đánh dấu là bẩn, máy ảo đang thực hiện IO, v.v. Phát sinh chi phí đó một lần, thay vì hai lần, là một tối ưu hóa rõ ràng.

Một số số

Tôi tập hợp các bài kiểm tra nhỏ sau đây:

public class ConsolePerf {

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            benchmark("Warm " + i);
        }
        benchmark("real");
    }

    private static void benchmark(String string) {
        benchString(string + "short", "This is a short String");
        benchString(string + "long", "This is a long String with a number of newlines\n"
                  + "in it, that should simulate\n"
                  + "printing some long sentences and log\n"
                  + "messages.");

    }

    private static final int REPS = 1000;

    private static void benchString(String name, String value) {
        long time = System.nanoTime();
        for (int i = 0; i < REPS; i++) {
            System.out.println(value);
        }
        double ms = (System.nanoTime() - time) / 1000000.0;
        System.err.printf("%s run in%n    %12.3fms%n    %12.3f lines per ms%n    %12.3f chars per ms%n",
                name, ms, REPS/ms, REPS * (value.length() + 1) / ms);

    }


}

Mã này tương đối đơn giản, nó liên tục in một chuỗi ngắn hoặc một chuỗi dài để xuất ra. Chuỗi dài có nhiều dòng mới trong đó. Nó đo thời gian cần thiết để in 1000 lần lặp mỗi lần.

Nếu tôi chạy nó tại dấu nhắc lệnh unix (Linux) và chuyển hướng STDOUTđến /dev/nullvà in kết quả thực tế đến STDERR, tôi có thể làm như sau:

java -cp . ConsolePerf > /dev/null 2> ../errlog

Đầu ra (in errlog) trông giống như:

Warm 0short run in
           7.264ms
         137.667 lines per ms
        3166.345 chars per ms
Warm 0long run in
           1.661ms
         602.051 lines per ms
       74654.317 chars per ms
Warm 1short run in
           1.615ms
         619.327 lines per ms
       14244.511 chars per ms
Warm 1long run in
           2.524ms
         396.238 lines per ms
       49133.487 chars per ms
.......
Warm 99short run in
           1.159ms
         862.569 lines per ms
       19839.079 chars per ms
Warm 99long run in
           1.213ms
         824.393 lines per ms
      102224.706 chars per ms
realshort run in
           1.204ms
         830.520 lines per ms
       19101.959 chars per ms
reallong run in
           1.215ms
         823.160 lines per ms
      102071.811 chars per ms

Điều đó có nghĩa là gì? Hãy để tôi nhắc lại 'khổ thơ' cuối cùng:

realshort run in
           1.204ms
         830.520 lines per ms
       19101.959 chars per ms
reallong run in
           1.215ms
         823.160 lines per ms
      102071.811 chars per ms

Điều đó có nghĩa là, đối với tất cả các ý định và mục đích, mặc dù dòng 'dài' dài hơn khoảng 5 lần và chứa nhiều dòng mới, nó chỉ mất khoảng thời gian để xuất ra như dòng ngắn.

Số lượng ký tự mỗi giây trong thời gian dài gấp 5 lần và thời gian trôi qua là như nhau .....

Nói cách khác, hiệu suất của bạn có tỷ lệ tương ứng với số lượng bản in bạn có chứ không phải những gì họ in.

Cập nhật: Điều gì xảy ra nếu bạn chuyển hướng đến một tệp, thay vì đến / dev / null?

realshort run in
           2.592ms
         385.815 lines per ms
        8873.755 chars per ms
reallong run in
           2.686ms
         372.306 lines per ms
       46165.955 chars per ms

Nó chậm hơn rất nhiều, nhưng tỷ lệ là như nhau ....


Đã thêm một số số hiệu suất.
rolfl

Bạn cũng phải xem xét vấn đề "\n"có thể không phải là dấu chấm hết dòng đúng. printlnsẽ tự động chấm dứt dòng với (các) ký tự bên phải, nhưng việc gắn \ntrực tiếp vào chuỗi của bạn có thể gây ra sự cố. Nếu bạn muốn làm đúng, bạn có thể cần sử dụng định dạng chuỗi hoặc thuộc tính line.separatorhệ thống . printlnsạch hơn nhiều
user2357112 hỗ trợ Monica

3
Đây là tất cả các phân tích tuyệt vời vì vậy chắc chắn +1, nhưng tôi lập luận rằng một khi bạn cam kết đầu ra giao diện điều khiển, những khác biệt hiệu suất nhỏ này bay ra khỏi cửa sổ. Nếu thuật toán của chương trình của bạn chạy nhanh hơn kết quả đầu ra (ở mức đầu ra nhỏ này), bạn có thể in từng ký tự một và không nhận thấy sự khác biệt.
David Harkness

Tôi tin rằng đây là một sự khác biệt giữa Java và C / C ++ mà đầu ra được đồng bộ hóa. Tôi nói điều này bởi vì tôi nhớ lại việc viết một chương trình đa luồng và có vấn đề với đầu ra bị cắt xén nếu các luồng khác nhau cố gắng viết để ghi vào bàn điều khiển. Bất cứ ai có thể xác minh điều này?

6
Điều quan trọng cũng cần nhớ là không có bất kỳ tốc độ nào trong số đó có vấn đề khi đặt ngay bên cạnh chức năng chờ vào đầu vào của người dùng.
vmrob

2

Tôi không nghĩ rằng có một loạt các printlnvấn đề là một vấn đề thiết kế. Theo tôi thấy thì điều này rõ ràng có thể được thực hiện với bộ phân tích mã tĩnh nếu nó thực sự là một vấn đề.

Nhưng nó không phải là vấn đề vì hầu hết mọi người không làm IO như thế này. Khi họ thực sự cần thực hiện nhiều IO, họ sử dụng bộ đệm (BufferedReader, BufferedWriter, v.v.) khi đầu vào được đệm, bạn sẽ thấy hiệu suất tương tự nhau, bạn không cần phải lo lắng về việc có bó printlnhoặc ít println.

Vì vậy, để trả lời câu hỏi ban đầu. Tôi sẽ nói, không tệ nếu bạn sử dụng printlnđể in một vài thứ như hầu hết mọi người sẽ sử dụng printlncho.


1

Trong các ngôn ngữ cấp cao hơn như C và C ++, đây không phải là vấn đề so với Java.

Trước hết, C và C ++ định nghĩa phép nối chuỗi thời gian biên dịch, do đó bạn có thể có một cái gì đó như:

std::cout << "Good morning everyone. I am here today to present you with a very, "
    "very lengthy sentence in order to prove a point about how it looks strange "
    "amongst other code.";

Trong trường hợp như vậy, nối chuỗi không chỉ là một tối ưu hóa mà bạn có thể khá nhiều, thông thường (v.v.) phụ thuộc vào trình biên dịch để thực hiện. Thay vào đó, nó được yêu cầu trực tiếp bởi các tiêu chuẩn C và C ++ (giai đoạn 6 của bản dịch: "Các mã thông báo chuỗi ký tự liền kề được nối với nhau.").

Mặc dù phải trả thêm một chút phức tạp trong trình biên dịch và triển khai, C và C ++ làm thêm một chút để che giấu sự phức tạp của việc tạo đầu ra hiệu quả từ lập trình viên. Java giống như ngôn ngữ lắp ráp hơn - mỗi cuộc gọi để System.out.printlndịch trực tiếp hơn nhiều cuộc gọi đến hoạt động cơ bản để ghi dữ liệu vào bàn điều khiển. Nếu bạn muốn đệm để cải thiện hiệu quả, điều đó phải được cung cấp riêng.

Điều này có nghĩa là, ví dụ, trong C ++, viết lại ví dụ trước, thành một cái gì đó như thế này:

std::cout << "Good morning everyone. I am here today to present you with a very, ";
std::cout << "very lengthy sentence in order to prove a point about how it looks ";       
std::cout << "strange amongst other code.";

... thông thường 1 sẽ hầu như không ảnh hưởng đến hiệu quả. Mỗi lần sử dụng coutchỉ đơn giản là gửi dữ liệu vào một bộ đệm. Bộ đệm đó sẽ được xóa vào luồng bên dưới khi bộ đệm được lấp đầy hoặc mã đã cố đọc đầu vào từ việc sử dụng (chẳng hạn như với std::cin).

iostreams cũng có một thuộc sync_with_stdiotính xác định xem đầu ra từ iostreams có được đồng bộ hóa với đầu vào kiểu C hay không (ví dụ getchar:). Theo mặc định sync_with_stdiođược đặt thành đúng, vì vậy, ví dụ, nếu bạn viết vào std::cout, sau đó đọc qua getchar, dữ liệu bạn đã viết coutsẽ bị xóa khi getcharđược gọi. Bạn có thể đặt sync_with_stdiothành false để tắt tính năng đó (thường được thực hiện để cải thiện hiệu suất).

sync_with_stdiocũng kiểm soát một mức độ đồng bộ giữa các chủ đề. Nếu đồng bộ hóa được bật (mặc định) ghi vào iostream từ nhiều luồng có thể dẫn đến dữ liệu từ các luồng được xen kẽ, nhưng ngăn chặn mọi điều kiện cuộc đua. IOW, chương trình của bạn sẽ chạy và tạo đầu ra, nhưng nếu nhiều hơn một luồng ghi vào luồng cùng một lúc, việc xen kẽ dữ liệu từ các luồng khác nhau thường sẽ khiến đầu ra khá vô dụng.

Nếu bạn tắt đồng bộ hóa, thì đồng bộ hóa quyền truy cập từ nhiều luồng cũng hoàn toàn là trách nhiệm của bạn. Viết đồng thời từ nhiều luồng có thể / sẽ dẫn đến một cuộc đua dữ liệu, có nghĩa là mã có hành vi không xác định.

Tóm lược

C ++ mặc định cho một nỗ lực cân bằng tốc độ với an toàn. Kết quả khá thành công đối với mã đơn luồng, nhưng ít hơn đối với mã đa luồng. Mã đa luồng thường cần đảm bảo rằng chỉ có một luồng ghi vào luồng tại một thời điểm để tạo đầu ra hữu ích.


1. Có thể tắt bộ đệm cho luồng, nhưng thực tế làm như vậy là khá bất thường và khi / nếu ai đó làm điều đó, có lẽ vì một lý do khá cụ thể, chẳng hạn như đảm bảo rằng tất cả đầu ra được ghi lại ngay lập tức mặc dù ảnh hưởng đến hiệu suất . Trong mọi trường hợp, điều này chỉ xảy ra nếu mã thực hiện nó một cách rõ ràng.


13
" Trong các ngôn ngữ cấp cao hơn như C và C ++, đây không phải là vấn đề so với Java. " - cái gì? C và C ++ là các ngôn ngữ cấp thấp hơn Java. Ngoài ra, bạn đã quên thiết bị đầu cuối dòng của bạn.
user2357112 hỗ trợ Monica

1
Xuyên suốt tôi chỉ ra cơ sở khách quan cho Java là ngôn ngữ cấp thấp hơn. Không chắc chắn về những gì kết thúc dòng bạn đang nói về.
Jerry Coffin

2
Java cũng không kết hợp thời gian biên dịch. Ví dụ: "2^31 - 1 = " + Integer.MAX_VALUEđược lưu trữ dưới dạng một chuỗi được liên kết đơn (JLS Sec 3.10.515.28 ).
200_success

2
@ 200_success: Java thực hiện nối chuỗi trong thời gian biên dịch dường như giảm xuống §15,18.1: "Đối tượng Chuỗi mới được tạo (§12.5) trừ khi biểu thức là biểu thức hằng số thời gian biên dịch (§15.28)." Điều này dường như cho phép nhưng không yêu cầu việc ghép được thực hiện tại thời điểm biên dịch. Tức là, kết quả phải được tạo mới nếu các đầu vào không phải là hằng số thời gian biên dịch, nhưng không có yêu cầu nào được thực hiện theo một trong hai hướng nếu chúng là các hằng số thời gian biên dịch. Để yêu cầu ghép thời gian biên dịch, bạn phải đọc "nếu" thực sự có nghĩa là "nếu và chỉ khi".
Jerry Coffin

2
@Phoshi: Thử với các tài nguyên thậm chí không giống với RAII. RAII cho phép lớp quản lý tài nguyên, nhưng thử với tài nguyên yêu cầu mã máy khách để quản lý tài nguyên. Các tính năng (trừu tượng, chính xác hơn) mà người ta có và những thứ còn thiếu hoàn toàn phù hợp - thực tế, chúng chính xác là thứ làm cho một ngôn ngữ ở cấp độ cao hơn ngôn ngữ khác.
Jerry Coffin

1

Mặc dù hiệu suất không thực sự là một vấn đề ở đây, khả năng đọc kém của một loạt các printlncâu lệnh chỉ ra một khía cạnh thiết kế còn thiếu.

Tại sao chúng ta viết một chuỗi nhiều printlncâu? Nếu nó chỉ là một khối văn bản cố định, giống như một --helpvăn bản trong lệnh console, sẽ tốt hơn nhiều nếu có nó như một nguồn tài nguyên riêng biệt và đọc nó và viết nó ra màn hình theo yêu cầu.

Nhưng thông thường nó là một hỗn hợp của các phần năng động và tĩnh. Giả sử chúng ta có một số dữ liệu đơn hàng một mặt và mặt khác một số phần văn bản tĩnh cố định và những thứ này phải được trộn lẫn với nhau để tạo thành một bảng xác nhận đơn hàng. Một lần nữa, trong trường hợp này, tốt hơn là có một tệp văn bản ressource riêng: ressource sẽ là một mẫu, chứa một số loại ký hiệu (giữ chỗ), được thay thế trong thời gian chạy bằng dữ liệu thứ tự thực tế.

Việc tách ngôn ngữ lập trình khỏi ngôn ngữ tự nhiên có nhiều ưu điểm - trong số đó là quốc tế hóa: Bạn có thể phải dịch văn bản nếu bạn muốn trở nên đa ngôn ngữ với phần mềm của mình. Ngoài ra, tại sao một bước biên dịch cần thiết nếu bạn chỉ muốn sửa lỗi văn bản, nói sửa một số lỗi chính tả.

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.