Exception Handling in Java – The Bad Parts

Trong bài lần trước, Exception Handling in Java – The Good Parts, tôi đã nêu ra các lợi ích của việc sử dụng Exception Handling trong Java. Tuy vậy, nếu sử dụng sai chỗ, các Exception có thể khiến performance của chương trình bị ảnh hưởng ít nhiều.

Chắc hẳn trong chúng ta ai cũng có lần phải check xem chuỗi (String) nhập vào có phải số nguyên hay không, dù là làm bài tập, hay làm project. Tôi đã từng rất vui khi tìm ra một mẹo có thể xử lý vấn đề này một cách nhanh gọn như sau:

public boolean checkInt(String input) {
    try {
        Integer.parseInt(input);
        return true;
    } catch (NumberFormatException e) {
        return false;
    }
}

Cho đến khi tôi thấy method này:

public boolean isInteger(String str) {
    if (str == null) {
        return false;
    }
    int length = str.length();
    if (length == 0) {
        return false;
    }
    for (int i = 0; i < length; i++) {
        char c = str.charAt(i);
        if (c <= '/' || c >= ':') {
            return false;
        }
    }
    return true;
}

Vâng, tôi đã thử check xem cái method dài dài kia thì có gì hay hơn cái mẹo mà tôi vẫn hay dùng dù cùng công dụng là check xem có phải số nguyên hay không. Tôi tạo một vòng for và check 10 triệu lần:

Trường hợp chuỗi nhập vào là số nguyên:

public static void main(String[] args) {
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 10_000_000; i++) {
        checkInt("50");
    }
    long endTime = System.currentTimeMillis();
    System.out.println(endTime - startTime);

    startTime = System.currentTimeMillis();
    for (int i = 0; i < 10_000_000; i++) {
        isInteger("50");
    }
    endTime = System.currentTimeMillis();
    System.out.println(endTime - startTime);
}

Kết quả là 16 và 4. Cái method dài kia nhanh hơn method của tôi chẳng đáng là bao, mà code thì lằng nhằng. Hihi.

Trường hợp chuỗi nhập vào không phải số nguyên:

public static void main(String[] args) {
    //Generate startTime
    long startTime = System.currentTimeMillis();
    //Loop 10_000_000 times to check Integer.parseInt()
    for (int i = 0; i < 10_000_000; i++) {
        checkInt("a");
    }
    long endTime = System.currentTimeMillis();
    //Print out result
    System.out.println(endTime - startTime);

    //Generate startTime
    startTime = System.currentTimeMillis();
    //Loop 10_000_000 times to check isInteger()
    for (int i = 0; i < 10_000_000; i++) {
        isInteger("a");
    }
    endTime = System.currentTimeMillis();

    //Print out result
    System.out.println(endTime - startTime);
}

Tôi giật mình vì kết quả là 7166 và 3. Khoảng cách quá xa và chương trình chạy khá lâu. CPU i5 của tôi cũng sử dụng khoảng 40% trong quá trình chương trình chạy. Tại sao lại như vậy nhỉ?

1. Khái niệm Context-Switch

Khái niệm context-switch có thể rất lạ với một sinh viên chỉ học lập trình phần mềm mà không học gì về khoa học máy tính (như tôi). Bạn có thể hiểu như sau: Context-Switch là quá trình lưu và khôi phục luồng xử lý hiện tại của một process hoặc một thread. Như vậy luồng xử lý đó có thể chạy tiếp vào một thời điểm nào đó sau khi được lưu lại.

Context-switch có thể được sử dụng ở cả phần mềm và phần cứng. Nó là khái niệm cốt lõi của một hệ điều hành đa nhiệm. Trong các chương trình phần mềm, quá trình này sẽ xảy ra gián tiếp khi có bất kì process nào làm gián đoạn luồng xử lý của chương trình và chuyển qua một luồng xử lý khác.

2. Context-Switch trong Java và cái giả phải trả cho Exception-Handling

Dễ thấy, quá trình xử lý Exception của Java sẽ làm xảy ra gián tiếp quá trình Context-Switch, do flow của chương trình bị gián đoạn khi Exception được ném ra. Trong các chương trình Java, quá trình context-switch luôn luôn gây ra sự tốn kém: Non-Reentran Data, Heap + Stack paged out và được lưu lại để phục vụ cho việc quay trở lại ngẫu nhiên, và một môi trường mới sẽ được paged-in. (Bạn đọc hãy vào link wikipedia đọc để hiểu rõ nhất các khái niệm tôi đưa ra, vì khả năng dịch tiếng anh chuyên ngành của tôi có hạn. Rất xin lỗi các bạn).

Chốt lại: Rất tốn tài nguyên –> performance của chương trình bị ảnh hưởng.

3. Khi nào dùng Exception-Handling?

Đây là một câu hỏi không hề có câu trả lời cụ thể. Càng vào code nhiều, bạn sẽ càng hiểu được những trường hợp nào nên dùng, những trường hợp nào thì không. Tôi sẽ nêu ra một vài trường hợp bạn có thể nhận biết được sự nguy hiểm như sau:

  • Đặt try catch trong vòng lặp for – Rất nguy hiểm, cần cân nhắc kĩ. Bạn hoàn toàn có thể đặt for trong try/catch block, nhưng điều ngược lại thì cần phải xem có thực sự cần thiết không.
  • Nếu bạn buộc phải catch một exception và không làm gì với nó cả thì hãy dùng throws.

Quan điểm của mình:

  • Nếu lỗi là một lỗi phổ biến (check số nguyên, chia cho 0, người dùng nhập sai pass…blah…blah…) thì không catch exception
  • Nếu không thể làm gì để xử lý exception thì không catch nó.
8 Likes

Có 2 vấn đề mình thấy trong bài:

  • Tại sao khi ném exception thì xảy ra context switching? Any source?
  • Một nguyên nhân khác khá phổ biến khiến performance bị giảm là new object. Mình nghĩ đây phải là nguyên nhân chính trong trường hợp này.

Để kiểm chứng mục số 2 thì mình có code lại chương trình của bạn (theo code style của mình), và sửa lại hàm nhanh hơn bằng cách cho nó new 1 object khi false.

Và đây là kết quả mới.

Code mình để ở đây.

import java.util.function.Function;

public class Test {
	public static void main(String[] args) {
		// System.out.println(run(10_000_000, Test::isIntegerWithException, "50"));
		// System.out.println(run(10_000_000, Test::isIntegerWithoutException, "50"));
		System.out.println(run(10_000_000, Test::isIntegerWithException, "a"));
		System.out.println(run(10_000_000, Test::isIntegerWithoutException, "a"));
	}

	static long run(int count, Function<String, Boolean> func, String input) {
		long time = System.currentTimeMillis();
		for (int i = 0; i < count; i++)
			func.apply(input);
		return System.currentTimeMillis() - time;
	}

	static boolean isIntegerWithException(String str) {
		try {
			Integer.parseInt(str);
			return true;
		} catch (NumberFormatException e) {
			return false;
		}
	}

	static boolean isIntegerWithoutException(String str) {
		if (str == null || str.isEmpty()) {
			new NumberFormatException("Invalid");
			return false;
		}
		for (int i = 0; i < str.length(); i++) {
			char ch = str.charAt(i);
			if (ch < '0' || ch > '9') {
				new NumberFormatException("Invalid");
				return false;
			}
		}
		return true;
	}
}
6 Likes

Uhm, tớ nghĩ post #1, khi đề cập vụ context switching gây ra bad performance, là thông tin không chính xác.
Exception performance chậm chủ yếu do 2 nguyên nhân:

  • Java Stack trace của một exception được điền đầy, khi một exception object được tạo. Vì việc lần stack trace khá đắt đỏ, nên nếu exception của cậu được tung ra càng sâu trên stack trace, cost tạo ra exception object sẽ càng lớn.
    Đó là lý do method isIntegerWithoutException của @tonghoangvu chậm ngang ngửa với method tung exception. Cả 2 method này đều tạo exception object, nên stacktrace cũng được fill lúc đó.
    Chi tiết benchmark có ở mục See also.
  • Cost của stack unwinding sau khi kết thúc try block/catch block.
    Khoảng cách giữa exception throwing và exception catching trên call stack càng xa, thời gian bỏ ra cho stack unwinding càng lớn.
    Chi tiết xem ở mục See also.

Có một số điều tớ cũng muốn đề cập:

  • Cost exception đôi khi tốt hơn so với dùng flag ở một số điều kiện.
    Chi tiết xem ở benchmark article (mục See also).
  • Cost cho exception có thể tồi hơn flag trong đa số điều kiện, nhưng:
    • Exception khiến code dễ đọc hơn rất nhiều.
    • Exception stack trace là tính năng giúp việc lần lỗi trở nên dễ dàng hơn rất nhiều.
    • Cost của exception rơi vào khoảng vài nghìn ns (xem thêm benchmark ở mục See also). Nó không đáng kể so với việc tốn thời gian cho engineering cost (vài ngày để viết code phức tạp cho flag + thời gian vài giờ lần lỗi mỗi khi có vấn đề).
      Ngoài ra, exception flow nó không nên thường xuyên xảy ra so với main logic flow. Optimize exception bằng flag không cải thiện được bao nhiêu.

See also:

5 Likes

Tặng cậu một tym :heart_eyes: vì bài giải thích chi tiết và liên kết hữu ích.

2 Likes
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?