성능이 중요한 경우 Java의 String.format ()을 사용해야합니까?


215

로그 출력 등을 위해 항상 문자열을 작성해야합니다. JDK 버전을 통해 우리는 언제 StringBuffer(많은 추가, 스레드 안전) 및 StringBuilder(많은 추가, 비 스레드 안전) 을 사용 했는지 배웠습니다 .

사용에 대한 조언은 무엇입니까 String.format()? 효율적이거나 성능이 중요한 단일 라이너에 대한 연결을 고수해야합니까?

예를 들어 못생긴 구식,

String s = "What do you get if you multiply " + varSix + " by " + varNine + "?";

깔끔한 새 스타일 (String.format, 느릴 수 있음)

String s = String.format("What do you get if you multiply %d by %d?", varSix, varNine);

참고 : 내 구체적인 사용 사례는 코드 전체에서 수백 개의 '한 줄짜리'로그 문자열입니다. 그들은 루프를 포함하지 않으므로 StringBuilder너무 무겁습니다. String.format()특별히 관심이 있습니다.


28
왜 테스트하지 않습니까?
Ed S.

1
이 출력을 생성하는 경우 사람이 읽을 수있는 속도로 사람이 읽을 수 있어야한다고 가정합니다. 초당 최대 10 줄을 말하자. 어떤 접근 방식을 사용하든 문제가되지 않는다고 생각합니다. ;) 아니요, StringBuilder는 대부분의 상황에서 무겁지 않습니다.
Peter Lawrey

9
@ 피터, 아니 그것은 인간이 실시간으로 읽는 것이 절대 아닙니다! 문제가 발생했을 때 분석을 도와줍니다. 로그 출력은 일반적으로 초당 수천 줄이므로 효율적이어야합니다.
Air

5
초당 수천 줄을 생성하는 경우 1) 짧은 텍스트를 사용하고 일반 CSV와 같은 텍스트는 사용하지 않거나 바이너리를 사용하지 않는 것이 좋습니다 .2) 문자열을 전혀 사용하지 않으면 데이터를 만들지 않고 ByteBuffer에 데이터를 쓸 수 있습니다 모든 객체 (텍스트 또는 이진) 3) 디스크 또는 소켓에 데이터 쓰기를 배경으로합니다. 초당 약 1 백만 줄을 유지할 수 있어야합니다. (기본적으로 디스크 서브 시스템이 허용하는만큼) 10 배의 버스트를 달성 할 수 있습니다.
Peter Lawrey

7
이것은 일반적인 경우와 관련이 없지만 특히 로깅을 위해 LogBack (원래 Log4j 작성자가 작성) 은이 정확한 문제를 해결하는 매개 변수화 된 로깅 형식을 가지고 있습니다 -logback.qos.ch/manual/architecture.html#ParametrizedLogging
Matt Passell

답변:


123

나는 두 가지의 더 나은 성능을 가진 작은 클래스를 작성했으며 +는 형식보다 앞서 나갔다. 5-6의 요소로

import java.io.*;
import java.util.Date;

public class StringTest{

    public static void main( String[] args ){
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;

    for( i = 0; i< 100000; i++){
        String s = "Blah" + i + "Blah";
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<100000; i++){
        String s = String.format("Blah %d Blah", i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    }
}

다른 N에 대해 위를 실행하면 둘 다 선형으로 작동하지만 String.format5-30 배 느립니다.

그 이유는 현재 구현에서 String.format먼저 정규식으로 입력을 구문 분석 한 후 매개 변수를 채우기 때문입니다. 반면에 plus와의 연결은 JIT가 아닌 javac에 의해 최적화되어 StringBuilder.append직접 사용 됩니다.

런타임 비교


12
이 테스트에는 모든 문자열 형식을 완전히 나타내는 것이 아니라는 단점이 있습니다. 포함 할 항목과 특정 값을 문자열로 형식화하는 논리가 포함되는 경우가 종종 있습니다. 실제 테스트는 실제 시나리오를 살펴 봐야합니다.
Orion Adrian

9
SO + + StringBuffer에 대한 또 다른 질문이있었습니다. Java +의 최신 버전에서 가능한 경우 StringBuffer로 대체되었으므로 성능이 다르지 않습니다.
hhafez

25
이것은 매우 쓸모없는 방식으로 최적화되는 일종의 마이크로 벤치 마크와 매우 비슷합니다.
David H. Clements

20
잘못 구현 된 또 다른 마이크로 벤치 마크. 두 방법 모두 규모별로 어떻게 확장됩니까? 100, 1000, 10000, 1000000, 연산을 사용하는 것은 어떻습니까. 격리 된 코어에서 실행되지 않는 응용 프로그램에서 한 번의 테스트를 한 번만 실행하는 경우 컨텍스트 전환, 백그라운드 프로세스 등으로 인해 차이의 일부를 '부작용'으로 기록 할 수있는 방법을 알 수있는 방법이 없습니다.
Evan Plaice

8
또한 JIT에서
벗어나지

241

hhafez 코드를 가져 와서 메모리 테스트를 추가했습니다 .

private static void test() {
    Runtime runtime = Runtime.getRuntime();
    long memory;
    ...
    memory = runtime.freeMemory();
    // for loop code
    memory = memory-runtime.freeMemory();

각 접근 방식, '+'연산자, String.format 및 StringBuilder (toString () 호출)에 대해 별도로 실행하므로 사용 된 메모리는 다른 접근 방식의 영향을받지 않습니다. 더 많은 연결을 추가하여 문자열을 "Blah"+ i + "Blah"+ i + "Blah"+ i + "Blah"로 만듭니다.

결과는 다음과 같습니다 (평균 5 회 실행) :
접근 시간 (ms) 메모리 할당 (긴)
'+'연산자 747 320,504
문자열 형식 16484 373,312
StringBuilder 769 57,344

우리는 String '+'와 StringBuilder가 실제로 시간적으로 동일하다는 것을 알 수 있지만 StringBuilder는 메모리 사용에서 훨씬 더 효율적입니다. 가비지 콜렉터가 '+'연산자로 인해 많은 문자열 인스턴스를 정리하지 못할 정도로 짧은 시간 간격으로 많은 로그 호출 (또는 문자열과 관련된 기타 명령문)이있는 경우 매우 중요합니다.

BTW라는 메모 는 메시지를 작성하기 전에 로깅 수준 을 확인하는 것을 잊지 마십시오 .

결론 :

  1. 계속해서 StringBuilder를 사용하겠습니다.
  2. 나는 시간이 너무 많거나 인생이 적다.

8
"메시지를 구성하기 전에 로깅 수준을 확인하는 것을 잊지 마십시오"는 좋은 충고입니다. 디버그 메시지는 많을 수 있으며 프로덕션 환경에서는 활성화되지 않아야하기 때문에 디버그 메시지에 대해서는이 작업을 수행해야합니다.
stivlo

39
아니요, 이것은 옳지 않습니다. 무뚝뚝한 것은 유감이지만 유언장의 수는 놀라 울 정도로 짧지 않습니다. +연산자를 사용하면 동등한 StringBuilder코드로 컴파일됩니다 . 이와 같은 마이크로 벤치 마크는 성능을 측정하는 좋은 방법이 아닙니다 .jvisualvm을 사용하지 않는 이유는 jdk에 있습니다. 속도 String.format() 느리지 만 객체 할당이 아닌 형식 문자열을 구문 분석하는 시간 때문에 발생합니다. 필요할 때까지 로깅 아티팩트 생성을 연기하는 것은 좋은 조언이지만 성능에 영향을 줄 경우 잘못된 위치에 있습니다.
CurtainDog

1
@CurtainDog, 귀하의 의견은 4 살짜리 게시물에 작성되었습니다. 차이점을 해결하기 위해 문서를 가리 키거나 별도의 답변을 작성할 수 있습니까?
kurtzbot

1
@CurtainDog의 의견을 지원하는 참조 : stackoverflow.com/a/1532499/2872712 . 즉, 루프에서 수행되지 않는 한 +가 선호됩니다.
살구

And a note, BTW, don't forget to check the logging level before constructing the message.좋은 조언이 아닙니다. 우리가 java.util.logging.*구체적으로 이야기하고 있다고 가정하면 , 로깅 수준을 확인하는 것은 프로그램이 적절한 수준으로 로깅을 설정하지 않은 경우 원하지 않는 프로그램에 부정적인 영향을 줄 수있는 고급 처리를 수행하는 것에 대한 이야기입니다. 문자열 형식화는 AT ALL 처리 유형이 아닙니다. 포맷팅은 java.util.logging프레임 워크의 일부 이며, 로거 자체는 포맷터가 호출되기 전에 로깅 레벨을 확인합니다.
searchengine27

30

여기에 제시된 모든 벤치 마크에는 몇 가지 결함 이 있으므로 결과가 신뢰할 수 없습니다.

아무도 벤치마킹에 JMH 를 사용 하지 않았다는 사실에 놀랐습니다 .

결과 :

Benchmark             Mode  Cnt     Score     Error  Units
MyBenchmark.testOld  thrpt   20  9645.834 ± 238.165  ops/s  // using +
MyBenchmark.testNew  thrpt   20   429.898 ±  10.551  ops/s  // using String.format

단위는 초당 작업 일수록 더 좋습니다. 벤치 마크 소스 코드 . OpenJDK IcedTea 2.5.4 Java 가상 머신이 사용되었습니다.

따라서 이전 스타일 (+ 사용)이 훨씬 빠릅니다.


5
"+"이고 "format"인 주석을 달면 해석하기가 훨씬 쉬울 것입니다.
AjahnCharles 15 1-53에

21

이전의 추악한 스타일은 JAVAC 1.6에서 다음과 같이 자동으로 컴파일됩니다.

StringBuilder sb = new StringBuilder("What do you get if you multiply ");
sb.append(varSix);
sb.append(" by ");
sb.append(varNine);
sb.append("?");
String s =  sb.toString();

따라서 이것과 StringBuilder 사용 사이에는 아무런 차이가 없습니다.

String.format은 새로운 Formatter를 만들고 입력 형식 문자열을 구문 분석하고 StringBuilder를 만들고 모든 것을 추가하고 toString ()을 호출하기 때문에 훨씬 더 무겁습니다.


가독성 측면에서 게시 한 코드는 String.format ( "% d에 % d를 곱하면 무엇을 얻습니까?", varSix, varNine)보다 훨씬 더 복잡합니다.;
dusktreader

12
간의 차이 없음 +StringBuilder참. 불행히도이 스레드에는 다른 답변에 많은 잘못된 정보가 있습니다. 질문을 (으)로 변경하려고합니다 how should I not be measuring performance.
CurtainDog

12

Java의 String.format은 다음과 같이 작동합니다.

  1. 형식 문자열을 구문 분석하여 형식 청크 목록으로 분해합니다.
  2. 형식 청크를 반복하여 StringBuilder로 렌더링합니다. StringBuilder는 기본적으로 새 배열로 복사하여 필요에 따라 크기를 조정하는 배열입니다. 우리는 아직 최종 문자열을 얼마나 큰지 알지 못하기 때문에 필요합니다.
  3. StringBuilder.toString ()은 내부 버퍼를 새 문자열로 복사합니다.

이 데이터의 최종 대상이 스트림 인 경우 (예 : 웹 페이지 렌더링 또는 파일 쓰기) 스트림에 형식 청크를 직접 어셈블 할 수 있습니다.

new PrintStream(outputStream, autoFlush, encoding).format("hello {0}", "world");

옵티마이 저가 형식 문자열 처리를 최적화하지 않을 것이라고 추측합니다. 그렇다면 String.format을 StringBuilder로 수동으로 언 롤링 하는 것과 동등한 상각 성능을 유지합니다.


5
형식 문자열 처리 최적화에 대한 귀하의 추측이 맞지 않다고 생각합니다. Java 7을 사용하는 실제 테스트 String.format에서 내부 루프 (수백만 번 실행) 를 사용하면 실행 시간의 10 % 이상이에 걸린 것으로 나타났습니다 java.util.Formatter.parse(String). 이것은 내부 루프에서 호출 Formatter.format또는 호출하는 것을 피해야 함을 나타냅니다 PrintStream.format(Java의 표준 라이브러리, IMO의 결함, 특히 구문 분석 된 형식 문자열을 캐시 할 수 없기 때문에).
Andy MacKinlay

8

위의 첫 번째 답변을 확장 / 수정하기 위해 String.format이 실제로 도움이되는 번역은 아닙니다.
String.format이 도움이되는 것은 현지화 (l10n) 차이가있는 날짜 / 시간 (또는 숫자 형식 등)을 인쇄 할 때입니다 (일부 국가는 04Feb2009를 인쇄하고 다른 국가는 Feb042009를 인쇄합니다).
번역에서는 ResourceBundle 및 MessageFormat을 사용하여 올바른 언어에 적합한 번들을 사용할 수 있도록 외부화 가능한 문자열 (예 : 오류 메시지 및 기타)을 속성 번들로 이동하는 것에 대해서만 이야기하고 있습니다.

위의 모든 것을 살펴보면 성능 ​​측면에서 String.format 대 일반 연결이 선호하는 것입니다. 연결보다 .format에 대한 호출을보고 싶다면 반드시 그렇게하십시오.
결국, 코드는 작성된 것보다 훨씬 많이 읽습니다.


1
성능 측면에서 String.format 대 일반 연결은 이것이 잘못되었다고 생각하는 것으로 나타납니다. 성능 측면에서는 연결이 훨씬 좋습니다. 자세한 내용은 내 답변을 살펴보십시오.
Adam Stelmaszczyk 2016 년

6

귀하의 예에서 성능 probalby는 그다지 다르지 않지만 고려해야 할 다른 문제가 있습니다 : 즉 메모리 조각화. 연결 작업조차도 임시 문자열 인 경우에도 새 문자열을 생성합니다 (GC에 시간이 걸리고 더 많은 작업이 필요합니다). String.format ()은 더 읽기 쉽고 조각화가 적습니다.

또한 특정 형식을 많이 사용하는 경우 Formatter () 클래스를 직접 사용할 수 있습니다 (모든 String.format ()이 사용하는 것은 Formatter 인스턴스를 인스턴스화하는 것입니다).

또한, 당신이 알아야 할 다른 것 : substring () 사용에주의하십시오. 예를 들면 다음과 같습니다.

String getSmallString() {
  String largeString = // load from file; say 2M in size
  return largeString.substring(100, 300);
}

Java 하위 문자열이 작동하는 방식이기 때문에 큰 문자열은 여전히 ​​메모리에 있습니다. 더 나은 버전은 다음과 같습니다.

  return new String(largeString.substring(100, 300));

또는

  return String.format("%s", largeString.substring(100, 300));

두 번째 양식은 다른 작업을 동시에 수행하는 경우 더 유용합니다.


8
"관련 질문"을 지적 할 가치는 실제로 C #이므로 적용 할 수 없습니다.
Air

메모리 조각화를 측정하기 위해 어떤 도구를 사용했으며 조각화가 램의 속도 차이를 만들어 냅니까?
kritzikratzi

하위 문자열 메소드가 Java 7 +에서 변경되었음을 지적 할 가치가 있습니다. 이제 하위 문자열 문자 만 포함하는 새 문자열 표현을 반환해야합니다. 즉, 호출을 반환 할 필요가 없습니다. String :: new
João Rebelo

5

일반적으로 String.Format은 비교적 빠르며 세계화를 지원하므로 실제로 사용자가 읽은 것을 작성한다고 가정하면 String.Format을 사용해야합니다. 또한 하나의 문자열을 문장 당 3 개 이상으로 번역하려는 경우 (특히 문법 구조가 크게 다른 언어의 경우) 세계화하기가 더 쉽습니다.

이제 아무것도 번역 할 계획이 없다면 Java의 내장 + 연산자를로 변환하는 것에 의존하십시오 StringBuilder. 또는 Java를 StringBuilder명시 적으로 사용하십시오.


3

로깅 관점에서만 또 다른 관점.

이 스레드에 로그온하는 것과 관련된 많은 토론을 보았으므로 답변에 경험을 추가하는 것을 생각했습니다. 누군가 유용 할 수 있습니다.

포맷터를 사용하여 로깅하는 동기는 문자열 연결을 피하는 것에서 비롯된 것 같습니다. 기본적으로, 로그를 기록하지 않을 경우 문자열 연결의 오버 헤드를 원하지 않습니다.

로그하지 않으려면 실제로 연결 / 포맷 할 필요가 없습니다. 이와 같은 방법을 정의하면 말할 수 있습니다.

public void logDebug(String... args, Throwable t) {
    if(debugOn) {
       // call concat methods for all args
       //log the final debug message
    }
}

이 방법에서는 cancat / formatter가 디버그 메시지이고 debugOn = false 인 경우 실제로 호출되지 않습니다.

여기에서 포맷터 대신 StringBuilder를 사용하는 것이 여전히 낫습니다. 주요 동기는 그 중 하나를 피하는 것입니다.

동시에 각 로깅 문에 "if"블록을 추가하는 것을 좋아하지 않습니다.

  • 가독성에 영향을 미칩니다
  • 단위 테스트의 적용 범위를 줄입니다. 모든 라인을 테스트하려는 경우 혼동됩니다.

따라서 위와 같은 메소드로 로깅 유틸리티 클래스를 작성하고 성능 적중 및 이와 관련된 다른 문제에 대해 걱정하지 않고 어디에서나 사용하는 것을 선호합니다.


slf4j-api와 같은 기존 라이브러리를 활용하여 매개 변수화 된 로깅 기능을 사용하여이 사용 사례를 처리 할 수 ​​있습니까? slf4j.org/faq.html#logging_performance
ammianus

2

방금 hhafez의 테스트를 StringBuilder를 포함하도록 수정했습니다. XP에서 jdk 1.6.0_10 클라이언트를 사용하면 StringBuilder가 String.format보다 33 배 빠릅니다. -server 스위치를 사용하면 계수가 20으로 낮아집니다.

public class StringTest {

   public static void main( String[] args ) {
      test();
      test();
   }

   private static void test() {
      int i = 0;
      long prev_time = System.currentTimeMillis();
      long time;

      for ( i = 0; i < 1000000; i++ ) {
         String s = "Blah" + i + "Blah";
      }
      time = System.currentTimeMillis() - prev_time;

      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         String s = String.format("Blah %d Blah", i);
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);

      prev_time = System.currentTimeMillis();
      for ( i = 0; i < 1000000; i++ ) {
         new StringBuilder("Blah").append(i).append("Blah");
      }
      time = System.currentTimeMillis() - prev_time;
      System.out.println("Time after for loop " + time);
   }
}

이것이 과감하게 들릴 수도 있지만, 절대 숫자가 매우 낮기 때문에 드문 경우에만 관련이 있다고 생각합니다 .1 백만 개의 간단한 String.format 호출의 경우 4 초입니다. 처럼.

업데이트 : 주석에서 sjbotha가 지적했듯이 StringBuilder 테스트는 final이 없기 때문에 유효하지 않습니다 .toString().

에서 정확한 속도 향상 요인 String.format(.)으로는 StringBuilder내 컴퓨터합니다 (16에서 23 -server스위치).


1
테스트는 루프만으로 먹은 시간을 고려하지 않아 유효하지 않습니다. 이를 포함하고 다른 모든 결과에서 최소한 빼야합니다 (예 : 상당한 백분율 일 수 있음).
cletus

나는 그것을했다, for 루프는 0ms가 걸린다. 그러나 시간이 걸리더라도 이는 요인을 증가시킬뿐입니다.
the.duckman

3
StringBuilder 테스트는 실제로 사용할 수있는 문자열을 제공하기 위해 끝에 toString ()을 호출하지 않기 때문에 유효하지 않습니다. 나는 이것을 추가했고 그 결과 StringBuilder는 +와 거의 같은 시간이 걸립니다. 추가 횟수를 늘리면 결국 더 저렴해질 것입니다.
Sarel Botha

1

다음은 hhafez 항목의 수정 된 버전입니다. 문자열 작성기 옵션이 포함되어 있습니다.

public class BLA
{
public static final String BLAH = "Blah ";
public static final String BLAH2 = " Blah";
public static final String BLAH3 = "Blah %d Blah";


public static void main(String[] args) {
    int i = 0;
    long prev_time = System.currentTimeMillis();
    long time;
    int numLoops = 1000000;

    for( i = 0; i< numLoops; i++){
        String s = BLAH + i + BLAH2;
    }
    time = System.currentTimeMillis() - prev_time;

    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        String s = String.format(BLAH3, i);
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

    prev_time = System.currentTimeMillis();
    for( i = 0; i<numLoops; i++){
        StringBuilder sb = new StringBuilder();
        sb.append(BLAH);
        sb.append(i);
        sb.append(BLAH2);
        String s = sb.toString();
    }
    time = System.currentTimeMillis() - prev_time;
    System.out.println("Time after for loop " + time);

}

}

루프 후 시간 391 루프 후 시간 4163 루프 후 시간 227


0

이에 대한 대답은 특정 Java 컴파일러가 생성하는 바이트 코드를 최적화하는 방법에 따라 다릅니다. 문자열은 변경 불가능하며 이론적으로 각 "+"작업은 새로운 작업을 생성 할 수 있습니다. 그러나 컴파일러는 긴 문자열을 빌드 할 때 중간 단계를 거의 확실하게 최적화합니다. 위의 두 코드 줄 모두 정확히 동일한 바이트 코드를 생성 할 수 있습니다.

실제로 아는 유일한 방법은 현재 환경에서 코드를 반복적으로 테스트하는 것입니다. 두 가지 방법으로 문자열을 반복적으로 연결하고 서로 어떻게 시간이 초과되는지 확인하는 QD 앱을 작성하십시오.


1
두 번째 예제의 바이트 코드는 반드시 String.format을 호출하지만 간단한 연결로 인해 충격을 받았습니다 . 컴파일러가 형식 문자열을 사용하여 구문 분석해야하는 이유는 무엇입니까?
Jon Skeet

"바이너리 코드"라고 말한 곳에서 "바이트 코드"를 사용했습니다. 모든 것이 jmps 및 movs로 내려 오면 정확히 동일한 코드 일 수 있습니다.
네 – 저 제이크.

0

"hello".concat( "world!" )연결에서 적은 수의 문자열 사용 을 고려하십시오 . 다른 접근 방식보다 성능이 더 좋을 수 있습니다.

3 개 이상의 문자열이있는 경우 사용하는 컴파일러에 따라 StringBuilder 또는 String 만 사용하는 것을 고려하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.