문제
String.matches(regex) 는 왜 반복사용시 Pattern.matcher(string).matches() 보다 느릴까
깃허브에서 추천을 타고타고 보다보면 다른 사람들이 코드리뷰 해놓은 것들도 볼 수 있는데, 그중에 종종 보이는게 String.matches는 느려서 Pattern 을 static 으로 해두고 써야해요! 라는 내용이 있다.
ㅈㅎ님 코드리뷰를 하던 중에 마침 String.matches 를 써둔 게 있길래 리뷰의 근거를 마련하기 위해? 얼마나 느린지, 왜 느린지를 정리해보려고 한다.
private static final Pattern PATTERN = Pattern.compile(".*[0-9].*");
static Function<String, Void> validateNumberWithPattern = carName -> {
if (PATTERN.matcher(carName).matches()) {
throw new IllegalArgumentException();
}
return null;
};
static Function<String, Void> validateNumberWithString = carName -> {
if (carName.matches(".*[0-9].*")) {
throw new IllegalArgumentException();
}
return null;
};
static long checkTime(String title, int count, Function<String, Void> func) {
long start = System.currentTimeMillis();
func.apply("abc");
for (int i = 0; i < count; i++) {
func.apply("abc");
}
long end = System.currentTimeMillis();
System.out.println(title + " : " + (end - start));
return end - start;
}
validateNumberWithPattern 은 미리 정규표현식을 컴파일 해둔 뒤 패턴 매칭을 하는 메서드다.
validateNumberWithString 은 문자열을 받아 String.matches(regex) 을 실행하는 메서드다.
public class StringPatternTest {
static final int 오백만 = 500_0000;
public static void main(String[] args) {
List<Long> stringTime = new ArrayList<>();
List<Long> patternTime = new ArrayList<>();
for (int i = 0; i < 100; i++) {
stringTime.add(checkTime("String.matches", 오백만, validateNumberWithString));
patternTime.add(checkTime("Pattern.matches", 오백만, validateNumberWithPattern));
}
double stringAvg = stringTime.stream().mapToDouble(Long::doubleValue).average().getAsDouble();
double patternAvg = patternTime.stream().mapToDouble(Long::doubleValue).average().getAsDouble();
System.out.println("stringAvg = " + stringAvg);
System.out.println("patternAvg = " + patternAvg);
}
}
stringAvg = 587.85
patternAvg = 127.34
결과
대충 코드에 대해 설명하자면, 숫자인 문자열이 걸리면 예외를 발생시키는 함수에 문자열만 500만번 정규표현식 돌려보는 것을 1회로, 그걸 100회 돌리고 평균 시간을 구해보는 코드다.
String.matches는 Pattern 대비 약 5배가 더 걸린다.
String.matches
그래서 String.matches 내부를 봐봤다.
public boolean matches(String regex) {
return Pattern.matches(regex, this);
}
String.matches(regex) 내부는 Pattern.matches 를 실행하고 있다.
public static boolean matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
Pattern.matches(String regex, CharSequence input) 을 봐보면,
내부에서 Pettern.complie(regex) 로 정규표현식을 컴파일하고, 그렇게 나온 Pettern 객체로 문자열 input을 matcher 메서드로 검사 후, Matcher.matches 메서드를 실행하고 있다.
참고로 String은 CharSequence 인터페이스를 구현하고 있다.
public static Pattern compile(String regex) {
return new Pattern(regex, 0);
}
여기서 문제가 발생한다. compile 메서드는 새로운 Pettern 인스턴스를 생성한다!
즉, String.matches(regex) 를 돌리는 횟수만큼 새로운 Pettern 인스턴스를 만든다.
한번 정규표현식 검사하고 말거면 상관없겠지만, 보통은 여러 번 반복하며 재사용되기 때문에 매번 인스턴스를 만드는 것은 시간과 공간의 낭비다. 따라서 맨 위의 validateNumberWithPattern 메서드처럼, 미리 컴파일한 Pettern 인스턴스를 static final 로 필드를 만들어두고, 이를 재사용하는 것이 성능에 좋다.
정리
- String.matches(regex) 는 내부적으로 매 실행마다 Pettern 인스턴스를 생성한다.
- 따라서 매번 생성되는 Pettern 인스턴스를 static final 필드로 만들어 재사용하는 것이 약 5배 빠르다.
'TIL ✍️' 카테고리의 다른 글
23년 11월 7일(화요일) - 27번째 TIL (0) | 2023.11.07 |
---|---|
23년 11월 6일(월요일) - 26번째 TIL (0) | 2023.11.06 |
23년 11월 2일(목요일) - 24번째 TIL (0) | 2023.11.02 |
23년 11월 1일(수요일) - 23번째 TIL : String.repeat vs StringBuilder.append 속도 차이 (2) | 2023.11.01 |
23년 10월 31일(화요일) - 22번째 TIL (1) | 2023.10.31 |