Java 문자열에서 인쇄 할 수없는 모든 문자를 제거하는 가장 빠른 방법


81

StringJava 에서 인쇄 할 수없는 모든 문자를 제거하는 가장 빠른 방법은 무엇입니까 ?

지금까지 138 바이트, 131 자 문자열을 시도하고 측정했습니다.

  • 문자열 replaceAll()- 가장 느린 방법
    • 517009 결과 / 초
  • 패턴을 미리 컴파일 한 다음 Matcher의 replaceAll()
    • 637836 결과 / 초
  • StringBuffer 사용, codepointAt()하나씩 사용하여 코드 포인트 가져 오기 및 StringBuffer에 추가
    • 711946 개의 결과 / 초
  • StringBuffer를 사용하고, charAt()하나씩 사용하여 문자를 얻고 StringBuffer에 추가하십시오.
    • 1052964 결과 / 초
  • char[]버퍼를 미리 할당하고 , charAt()하나씩 사용하여 문자를 가져 와서이 버퍼를 채운 다음 다시 문자열로 변환합니다.
    • 2022653 개의 결과 / 초
  • 2 개 미리 할당 char[]버퍼 - 사용하여 한 번에 과거와는 문자열을 기존의 모든 문자를 얻을 getChars()반복,이 위에 오래 된 버퍼 일대일로 새로운 버퍼를 채우기, 다음 문자열에 새로운 버퍼를 변환 - 내 자신의 빠른 버전을
    • 2502502 개 결과 / 초
  • 만 사용 -이 버퍼와 같은 것들 byte[], getBytes()그리고 "UTF-8"로 인코딩을 지정
    • 857485 결과 / 초
  • 2 개의 byte[]버퍼가있는 동일한 항목 이지만 인코딩을 상수로 지정Charset.forName("utf-8")
    • 791076 개의 결과 / 초
  • 2 개의 byte[]버퍼가있는 동일한 항목 이지만 인코딩을 1 바이트 로컬 인코딩으로 지정 (간신히 수행해야 할 일)
    • 370164 결과 / 초

최선의 시도는 다음과 같습니다.

    char[] oldChars = new char[s.length()];
    s.getChars(0, s.length(), oldChars, 0);
    char[] newChars = new char[s.length()];
    int newLen = 0;
    for (int j = 0; j < s.length(); j++) {
        char ch = oldChars[j];
        if (ch >= ' ') {
            newChars[newLen] = ch;
            newLen++;
        }
    }
    s = new String(newChars, 0, newLen);

더 빨리 만드는 방법에 대한 의견이 있으십니까?

매우 이상한 질문에 답하는 데 대한 보너스 포인트 : "utf-8"문자셋 이름을 사용하면 사전 할당 된 정적 const를 사용하는 것보다 직접적으로 더 나은 성능을 얻을 수있는 이유는 Charset.forName("utf-8")무엇입니까?

최신 정보

  • 래칫 괴물의 제안 은 인상적인 3105590 결과 / 초 성능, + 24 % 향상을 제공합니다!
  • Ed Staub의 제안은 또 다른 개선을 가져 왔습니다. 3471017 결과 / 초, 이전 최고 대비 + 12 %.

업데이트 2

나는 모든 제안 된 솔루션과 그 교차 돌연변이를 수집하기 위해 최선을 다 했으며이를 github에 작은 벤치마킹 프레임 워크 로 게시했습니다 . 현재 17 개의 알고리즘이 있습니다. 그중 하나는 "특별한" -Voo1 알고리즘 ( SO 사용자 Voo 제공 )은 복잡한 반사 트릭을 사용하여 뛰어난 속도를 달성하지만 JVM 문자열의 상태를 엉망으로 만들어 별도로 벤치마킹됩니다.

확인하고 실행하여 상자에 대한 결과를 확인할 수 있습니다. 다음은 제가 얻은 결과의 요약입니다. 사양 :

  • 데비안 sid
  • Linux 2.6.39-2-amd64 (x86_64)
  • 패키지에서 설치된 Java sun-java6-jdk-6.24-1, JVM은 자신을 다음과 같이 식별합니다.
    • Java (TM) SE 런타임 환경 (빌드 1.6.0_24-b07)
    • Java HotSpot (TM) 64 비트 서버 VM (빌드 19.1-b02, 혼합 모드)

서로 다른 알고리즘은 서로 다른 입력 데이터 세트가 주어지면 궁극적으로 서로 다른 결과를 보여줍니다. 3 가지 모드로 벤치 마크를 실행했습니다.

동일한 단일 문자열

이 모드는 StringSource클래스가 상수로 제공하는 동일한 단일 문자열에서 작동합니다 . 대결은 다음과 같습니다.

 Ops / s │ 알고리즘
──────────┼──────────────────────────────
6535947 │ Voo1
──────────┼──────────────────────────────
5350454 │ RatchetFreak2EdStaub1GreyCat1
5249 343 │ EdStaub1
5002501 │ EdStaub1GreyCat1
4859086 │ ArrayOfCharFromStringCharAt
4295532 │ RatchetFreak1
4045307 │ ArrayOfCharFromArrayOfChar
2790178 │ RatchetFreak2EdStaub1GreyCat2
2583311 │ RatchetFreak2
1274 859 │ StringBuilderChar
1138174 │ StringBuilderCodePoint
  994727 │ ArrayOfByteUTF8String
  918611 │ ArrayOfByteUTF8Const
  756 086 │ MatcherReplace
  598945 │ StringReplaceAll
  460 045 │ ArrayOfByteWindows1251

차트 형식 : (출처 : greycat.ru )동일한 단일 문자열 차트

여러 문자열, 문자열의 100 %에 제어 문자가 포함됨

소스 문자열 공급자는 (0..127) 문자 집합을 사용하여 많은 임의의 문자열을 미리 생성했습니다. 따라서 거의 모든 문자열에 하나 이상의 제어 문자가 포함되었습니다. 알고리즘은이 사전 생성 된 배열에서 라운드 로빈 방식으로 문자열을 수신했습니다.

 Ops / s │ 알고리즘
──────────┼──────────────────────────────
2123142 │ Voo1
──────────┼──────────────────────────────
1782214 │ EdStaub1
1776199 │ EdStaub1GreyCat1
1694628 │ ArrayOfCharFromStringCharAt
1481 481 │ ArrayOfCharFromArrayOfChar
1460067 │ RatchetFreak2EdStaub1GreyCat1
1438435 │ RatchetFreak2EdStaub1GreyCat2
1366 494 │ RatchetFreak2
1,349,710 │ RatchetFreak1
  893176 │ ArrayOfByteUTF8String
  817127 │ ArrayOfByteUTF8Const
  778 089 │ StringBuilderChar
  734754 │ StringBuilderCodePoint
  377 829 │ ArrayOfByteWindows1251
  224140 │ MatcherReplace
  211104 │ StringReplaceAll

차트 형식 : (출처 : greycat.ru )다중 스트링, 100 % 농도

여러 문자열, 문자열의 1 %에 제어 문자가 포함됨

이전과 동일하지만 제어 문자로 문자열의 1 % 만 생성되었습니다. 다른 99 %는 [32..127] 문자 집합을 사용하여 생성되었으므로 제어 문자를 전혀 포함 할 수 없습니다. 이 합성 부하는 제 자리에서이 알고리즘의 실제 적용에 가장 가깝습니다.

 Ops / s │ 알고리즘
──────────┼──────────────────────────────
3711952 │ Voo1
──────────┼──────────────────────────────
2851440 │ EdStaub1GreyCat1
2455796 │ EdStaub1
2426007 │ ArrayOfCharFromStringCharAt
2347969 │ RatchetFreak2EdStaub1GreyCat2
2242152 │ RatchetFreak1
2171 553 │ ArrayOfCharFromArrayOfChar
1922707 │ RatchetFreak2EdStaub1GreyCat1
1857010 │ RatchetFreak2
1,023 751 │ ArrayOfByteUTF8String
  939 055 │ StringBuilderChar
  907194 │ ArrayOfByteUTF8Const
  841963 │ StringBuilderCodePoint
  606465 │ MatcherReplace
  501555 │ StringReplaceAll
  381185 │ ArrayOfByteWindows1251

차트 형식 : (출처 : greycat.ru )다중 스트링, 1 % 농도

누가 가장 좋은 답변을 제공했는지 결정하기가 매우 어렵지만 실제 응용 프로그램이 Ed Staub에 의해 제공되고 영감을받은 실제 응용 프로그램을 고려할 때 그의 답변을 표시하는 것이 공정 할 것 같습니다. 참여 해주신 모든 분들께 감사 드리며, 귀하의 의견은 매우 유용하고 귀중했습니다. 상자에서 테스트 스위트를 자유롭게 실행하고 더 나은 솔루션을 제안하십시오 (JNI 솔루션을 사용하고 있습니까?).

참고 문헌


21
"이 질문은 연구 노력을 보여줍니다"-흠 ... 네, 통과합니다. +1
Gustav Barkefors 2011-08-23

7
StringBuilder보다 빠른 변두리에있을 것이다 StringBuffer당신이 태그 있기 때문에 해제 동기화이기 때문에, 난 그냥이 언급micro-optimization

2
@Jarrod 로버슨 : 좋아, 그렇게하자 모든 읽기 전용 최종 필드하고 추출 s.length()의 밖으로 for:-) 아니라 루프

3
공백 아래의 일부 문자는 인쇄 할 수 있습니다 (예 : \t\n. 127을 초과하는 많은 문자는 문자 집합에서 인쇄 할 수 없습니다.
Peter Lawrey

1
용량이있는 문자열 버퍼를 초기화 했 s.length()습니까?
래칫 괴물

답변:


11

스레드간에 공유되지 않는 클래스에이 메서드를 포함하는 것이 합리적이면 버퍼를 재사용 할 수 있습니다.

char [] oldChars = new char[5];

String stripControlChars(String s)
{
    final int inputLen = s.length();
    if ( oldChars.length < inputLen )
    {
        oldChars = new char[inputLen];
    }
    s.getChars(0, inputLen, oldChars, 0);

기타...

현재 최선의 사례를 이해하고 있기 때문에 20 % 정도의 큰 승리입니다.

이것이 잠재적으로 큰 문자열에 사용되어야하고 메모리 "누수"가 우려되는 경우 약한 참조를 사용할 수 있습니다.


좋은 생각입니다! 지금까지 초당 최대 3471017 개의 문자열을 가져 왔습니다. 즉, 이전 최고 버전보다 12 % 향상되었습니다.
GreyCat 2011-08-24

25

1 문자 배열을 사용하면 조금 더 잘 작동 할 수 있습니다.

int length = s.length();
char[] oldChars = new char[length];
s.getChars(0, length, oldChars, 0);
int newLen = 0;
for (int j = 0; j < length; j++) {
    char ch = oldChars[j];
    if (ch >= ' ') {
        oldChars[newLen] = ch;
        newLen++;
    }
}
s = new String(oldChars, 0, newLen);

그리고 나는 반복되는 전화를 피했습니다 s.length();

작동 할 수있는 또 다른 마이크로 최적화는

int length = s.length();
char[] oldChars = new char[length+1];
s.getChars(0, length, oldChars, 0);
oldChars[length]='\0';//avoiding explicit bound check in while
int newLen=-1;
while(oldChars[++newLen]>=' ');//find first non-printable,
                       // if there are none it ends on the null char I appended
for (int  j = newLen; j < length; j++) {
    char ch = oldChars[j];
    if (ch >= ' ') {
        oldChars[newLen] = ch;//the while avoids repeated overwriting here when newLen==j
        newLen++;
    }
}
s = new String(oldChars, 0, newLen);

1
감사! 귀하의 버전은 초당 3105590 문자열을 산출합니다.
GreyCat 2011-08-23

newLen++;: 사전 증가를 사용하는 것은 ++newLen;어떻습니까? -( ++j루프에서도). 여기 보라 : stackoverflow.com/questions/1546981/...
토마스

final이 알고리즘에 추가 하고 oldChars[newLen++]( ++newLen오류-전체 문자열이 1만큼 떨어져 있습니다!)를 사용하면 측정 가능한 성능 향상이 발생하지 않습니다 (즉, 다른 실행의 차이에 필적하는 ± 2..3 % 차이를
얻음

@grey 나는 다른 최적화와 다른 버전의 제작
래칫 괴물

2
흠! 그것은 훌륭한 아이디어입니다! 내 프로덕션 환경에서 문자열의 99.9 %는 실제로 스트리핑이 필요하지 않습니다 char[]. 스트리핑이 발생하지 않으면 첫 번째 할당 도 제거 하고 문자열을 그대로 반환하도록 개선 할 수 있습니다 .
GreyCat 2011-08-23

11

내 측정에 따르면 현재 최선의 방법 (사전 할당 된 배열을 사용하는 괴물의 솔루션)을 약 30 % 이겼습니다. 어떻게? 내 영혼을 팔아서.

지금까지 논의한 모든 사람들이 이것이 거의 모든 기본 프로그래밍 원칙을 위반한다는 것을 알고 있다고 확신합니다. 어쨌든 다음은 문자열의 사용 된 문자 배열이 다른 문자열간에 공유되지 않는 경우에만 작동합니다. 디버깅해야하는 사람이 누구든지 당신을 죽일 수있는 모든 권리를 가질 것입니다 (substring () 호출없이 리터럴 문자열에 이것을 사용하는 것입니다) 이것은 JVM이 외부 소스에서 읽은 고유 문자열을 인턴하는 이유를 알지 못하기 때문에 작동합니다.) 벤치 마크 코드가이를 수행하지 않는지 확인하는 것을 잊지 마십시오. 이는 매우 가능성이 높으며 분명히 리플렉션 솔루션에 도움이 될 것입니다.

어쨌든 우리는 간다 :

    // Has to be done only once - so cache those! Prohibitively expensive otherwise
    private Field value;
    private Field offset;
    private Field count;
    private Field hash;
    {
        try {
            value = String.class.getDeclaredField("value");
            value.setAccessible(true);
            offset = String.class.getDeclaredField("offset");
            offset.setAccessible(true);
            count = String.class.getDeclaredField("count");
            count.setAccessible(true);
            hash = String.class.getDeclaredField("hash");
            hash.setAccessible(true);               
        }
        catch (NoSuchFieldException e) {
            throw new RuntimeException();
        }

    }

    @Override
    public String strip(final String old) {
        final int length = old.length();
        char[] chars = null;
        int off = 0;
        try {
            chars = (char[]) value.get(old);
            off = offset.getInt(old);
        }
        catch(IllegalArgumentException e) {
            throw new RuntimeException(e);
        }
        catch(IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        int newLen = off;
        for(int j = off; j < off + length; j++) {
            final char ch = chars[j];
            if (ch >= ' ') {
                chars[newLen] = ch;
                newLen++;
            }
        }
        if (newLen - off != length) {
            // We changed the internal state of the string, so at least
            // be friendly enough to correct it.
            try {
                count.setInt(old, newLen - off);
                // Have to recompute hash later on
                hash.setInt(old, 0);
            }
            catch(IllegalArgumentException e) {
                e.printStackTrace();
            }
            catch(IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        // Well we have to return something
        return old;
    }

내 테스트 문자열의 경우 이전 변형 3477148.18ops/s과 비교 2616120.89ops/s합니다. 이길 수있는 유일한 방법은 C로 작성하거나 (아마도 그렇지 않을 수도 있음) 지금까지 아무도 생각하지 않은 완전히 다른 접근 방식을 사용하는 것입니다. 타이밍이 다른 플랫폼에서 안정적인지 확실하지 않지만 적어도 내 상자 (Java7, Win7 x64)에서 안정적인 결과를 생성합니다.


해결해 주셔서 감사합니다. 질문 업데이트를 확인하세요. 테스트 프레임 워크를 게시하고 17 개 알고리즘에 대해 3 개의 테스트 실행 결과를 추가했습니다. 알고리즘은 항상 최상위에 있지만 Java String의 내부 상태를 변경하여 "불변 문자열"계약을 깨뜨려 실제 응용 프로그램에서 사용하기가 매우 어렵습니다. 테스트 현명, 네, 최선의 결과이지만, 별도의 지명으로 발표 할 것 같습니다. :)
GreyCat

3
@GreyCat 예, 확실히 큰 문자열이 첨부되어 있으며 솔직히 현재 최상의 솔루션을 더 개선 할 수있는 눈에 띄는 방법이 없다고 확신하기 때문에 거의 작성했습니다. 잘 작동 할 것이라고 확신하는 상황이 있지만 (스트립하기 전에 하위 문자열이나 인턴 호출이 없음) 현재 핫스팟 버전에 대한 지식 때문입니다 (즉, IO에서 인턴 문자열을 읽지 않을 것입니다). 특히 유용합니다). 실제로 추가 x %가 필요한 경우 유용 할 수 있지만, 그렇지 않으면 얼마나 개선 할 수 있는지 확인하기위한 기준선이 더 많습니다.)
Voo

1
시간을 찾으면 JNI 버전을 사용해 보려고했지만 지금까지 사용 해본 적이 없어서 흥미로울 것입니다. 그러나 추가 호출 오버 헤드 (문자열이 너무 작음)와 JIT가 함수를 최적화하는 데 그렇게 어렵지 않아야한다는 사실 때문에 속도가 느려질 것이라고 확신합니다. new String()문자열이 변경되지 않은 경우 사용하지 마십시오 .하지만 이미 얻은 것 같습니다.
Voo

나는 이미 순수 C에서 똑같은 일을 시도해 보았고, 리플렉션 기반 버전에 비해 실제로 크게 개선되지 않았습니다. C 버전은 + 5..10 % 더 빠르게 실행되지만 그다지 좋지는 않습니다. 최소한 1.5x-1.7x 정도는 될 것이라고 생각했습니다.
GreyCat 2011-08-29

2

프로세서의 수량에 따라 작업을 여러 개의 병렬 하위 작업으로 분할 할 수 있습니다.


그래, 나도 생각했지만 내 상황에서 성능 향상을 얻지 못할 것입니다.이 스트리핑 알고리즘은 이미 대규모 병렬 시스템에서 호출 될 것입니다.
GreyCat 2011-08-23

2
게다가 50-100 바이트 문자열마다 처리를 위해 몇 개의 스레드를 포크하는 것은 엄청난 과잉 일 것이라고 생각할 수 있습니다.
GreyCat

예, 모든 작은 문자열에 대해 스레드를 포크하는 것은 좋은 생각이 아닙니다. 그러나로드 밸런서는 성능을 향상시킬 수 있습니다. BTW, 동기화로 인해 성능이 부족한 StringBuffer 대신 StringBuilder로 성능을 테스트 했습니까?
umbr

내 프로덕션 설정은 여러 개의 개별 프로세스를 생성하고 가능한 한 많은 병렬 CPU와 코어를 사용하므로 StringBuilder문제없이 어디에서나 자유롭게 사용할 수 있습니다 .
GreyCat 2011-08-23

2

나는 너무 자유롭고 다른 알고리즘에 대한 작은 벤치 마크를 작성했습니다. 완벽하지는 않지만 임의의 문자열 (기본적으로 약 32 / 200 % 인쇄 불가능)에 대해 주어진 알고리즘을 10000 번 이상 실행합니다. 그것은 GC, 초기화 등과 같은 것들을 처리해야합니다. 어떤 알고리즘도 많은 방해없이 적어도 한 번의 실행을 가져서는 안되는 오버 헤드가 너무 많지 않습니다.

특히 잘 문서화되어 있지는 않지만 오 잘. 여기에 우리 는 래칫 괴물의 알고리즘과 기본 버전을 모두 포함했습니다. 현재는 [0, 200) 범위의 균일하게 분산 된 문자로 200 자 긴 문자열을 무작위로 초기화합니다.


노력에 +
1-

@GreyCat 글쎄요,하지만 (어쨌든 기존 코드에서) 함께 던지는 것이 아마도 더 빠를 것입니다.)
Voo

1

IANA 저수준 자바 성능 중독자이지만 메인 루프를 풀 려고 했습니까? 일부 CPU가 병렬로 검사를 수행 할 수있는 것으로 보입니다.

또한 이것은 최적화에 대한 재미있는 아이디어가 있습니다.


(a) 이전 단계에서 알고리즘의 다음 단계에 대한 종속성이 있기 때문에 여기에서 어떤 언 롤링이 수행 될 수 있는지 의심 스럽습니다. (b) Java에서 수동 루프 언 롤링을 수행하여 뛰어난 결과를 생성하는 사람에 대해 들어 본 적이 없습니다. JIT는 일반적으로 작업에 적합한 것으로 판단되는 모든 것을 풀 때 잘 수행합니다. 제안과 링크에 감사드립니다. :)
GreyCat 2011-08-24

0

"utf-8"문자셋 이름을 사용하면 미리 할당 된 static const Charset.forName ( "utf-8")을 사용하는 것보다 직접적으로 더 나은 성능을 얻을 수있는 이유는 무엇입니까?

String#getBytes("utf-8")등 을 의미하는 경우 : Charset.forName("utf-8")문자 집합이 캐시되지 않은 경우 내부적으로 사용 되기 때문에 더 나은 캐싱을 제외하고는 빠르지 않아야합니다 .

한 가지는 다른 문자 집합을 사용하고 있거나 일부 코드가 투명하게 작동하지만 캐시 된 문자 집합 StringCoding이 변경되지 않는다는 것입니다.

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