성능이 중요한 경우 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.