La forma más rápida de eliminar todos los caracteres no imprimibles de una cadena Java

81

¿Cuál es la forma más rápida de eliminar todos los caracteres no imprimibles de a Stringen Java?

Hasta ahora lo he probado y medido en una cadena de 138 bytes y 131 caracteres:

  • String's replaceAll()- método más lento
    • 517009 resultados / seg
  • Precompile un patrón, luego use Matcher's replaceAll()
    • 637836 resultados / seg
  • Use StringBuffer, obtenga puntos de código codepointAt()uno por uno y añádalos a StringBuffer
    • 711946 resultados / seg
  • Use StringBuffer, obtenga caracteres charAt()uno por uno y agregue a StringBuffer
    • 1052964 resultados / seg
  • Preasigne un char[]búfer, obtenga caracteres charAt()uno por uno y llene este búfer, luego conviértalo de nuevo a String
    • 2022653 resultados / seg
  • Preasigne 2 char[]búferes: antiguo y nuevo, obtenga todos los caracteres para String existente a la vez usando getChars(), itere sobre el búfer antiguo uno por uno y llene el nuevo búfer, luego convierta el nuevo búfer en String: mi propia versión más rápida
    • 2502502 resultados / seg
  • Misma materia con 2 buffers - solamente utilizando byte[], getBytes()y que especifican codificación como "UTF-8"
    • 857485 resultados / seg
  • Lo mismo con 2 byte[]búferes, pero especificando la codificación como una constanteCharset.forName("utf-8")
    • 791076 resultados / seg
  • Lo mismo con 2 byte[]búferes, pero especificando la codificación como codificación local de 1 byte (apenas es una cosa sensata)
    • 370164 resultados / seg

Mi mejor intento fue el siguiente:

    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);

¿Alguna idea sobre cómo hacerlo aún más rápido?

Puntos de bonificación por responder una pregunta muy extraña: ¿por qué usar el nombre del juego de caracteres "utf-8" produce directamente un mejor rendimiento que usar la constante estática preasignada Charset.forName("utf-8")?

Actualizar

  • La sugerencia del fanático del trinquete produce un rendimiento impresionante de 3105590 resultados por segundo, ¡una mejora del + 24%!
  • La sugerencia de Ed Staub arroja otra mejora: 3471017 resultados / segundo, un + 12% sobre el mejor anterior.

Actualización 2

Hice todo lo posible para recopilar todas las soluciones propuestas y sus mutaciones cruzadas y las publiqué como un pequeño marco de evaluación comparativa en github . Actualmente tiene 17 algoritmos. Uno de ellos es "especial": el algoritmo Voo1 ( proporcionado por el usuario de SO Voo ) emplea intrincados trucos de reflexión para lograr velocidades estelares, pero estropea el estado de las cadenas JVM, por lo que se compara por separado.

Le invitamos a comprobarlo y ejecutarlo para determinar los resultados en su caja. Aquí hay un resumen de los resultados que obtuve con el mío. Son especificaciones:

  • Debian sid
  • Linux 2.6.39-2-amd64 (x86_64)
  • Java instalado desde un paquete sun-java6-jdk-6.24-1, JVM se identifica como
    • Entorno de ejecución Java (TM) SE (compilación 1.6.0_24-b07)
    • Servidor VM Java HotSpot (TM) de 64 bits (compilación 19.1-b02, modo mixto)

Los diferentes algoritmos muestran en última instancia resultados diferentes dado un conjunto diferente de datos de entrada. Ejecuté un punto de referencia en 3 modos:

La misma cuerda única

Este modo funciona en una misma cadena única proporcionada por la StringSourceclase como constante. El enfrentamiento es:

 Ops / s │ Algoritmo
──────────┼──────────────────────────────
6535 947 │ Voo1
──────────┼──────────────────────────────
5350454 │ RatchetFreak2EdStaub1GreyCat1
5249343 │ EdStaub1
5002501 │ EdStaub1GreyCat1
4859086 │ ArrayOfCharFromStringCharAt
4295532 │ RatchetFreak1
4045307 │ ArrayOfCharFromArrayOfChar
2790178 │ RatchetFreak2EdStaub1GreyCat2
2583311 │ RatchetFreak2
1274 859 │ StringBuilderChar
1138174 │ StringBuilderCodePoint
  994727 │ ArrayOfByteUTF8String
  918611 │ ArrayOfByteUTF8Const
  756086 │ MatcherReplace
  598945 │ StringReplaceAll
  460 045 │ ArrayOfByteWindows1251

En forma de gráficos: (fuente: greycat.ru )El mismo gráfico de una sola cuerda

Varias cadenas, el 100% de las cadenas contienen caracteres de control

El proveedor de cadenas de origen generó muchas cadenas aleatorias utilizando el juego de caracteres (0..127), por lo que casi todas las cadenas contenían al menos un carácter de control. Los algoritmos recibieron cadenas de esta matriz pregenerada en forma de turnos.

 Ops / s │ Algoritmo
──────────┼──────────────────────────────
2123142 │ Voo1
──────────┼──────────────────────────────
1782 214 │ EdStaub1
1776199 │ EdStaub1GreyCat1
1694628 │ ArrayOfCharFromStringCharAt
1484481 │ ArrayOfCharFromArrayOfChar
1460 067 │ RatchetFreak2EdStaub1GreyCat1
1438435 │ RatchetFreak2EdStaub1GreyCat2
1366494 │ RatchetFreak2
1349710 │ RatchetFreak1
  893176 │ ArrayOfByteUTF8String
  817127 │ ArrayOfByteUTF8Const
  778089 │ StringBuilderChar
  734754 │ StringBuilderCodePoint
  377829 │ ArrayOfByteWindows1251
  224140 │ MatcherReplace
  21114 │ StringReplaceAll

En forma de gráficos: (fuente: greycat.ru )Varias cadenas, 100% de concentración

Varias cadenas, el 1% de las cadenas contienen caracteres de control

Igual que antes, pero solo el 1% de las cadenas se generó con caracteres de control; el otro 99% se generó usando el conjunto de caracteres [32..127], por lo que no podían contener caracteres de control en absoluto. Esta carga sintética es la más cercana a la aplicación de este algoritmo en el mundo real en mi casa.

 Ops / s │ Algoritmo
──────────┼──────────────────────────────
3711952 │ Voo1
──────────┼──────────────────────────────
2851440 │ EdStaub1GreyCat1
2455 796 │ EdStaub1
2426007 │ ArrayOfCharFromStringCharAt
2347969 │ RatchetFreak2EdStaub1GreyCat2
2242152 │ RatchetFreak1
2 171 553 │ ArrayOfCharFromArrayOfChar
1922707 │ RatchetFreak2EdStaub1GreyCat1
1857 010 │ RatchetFreak2
1023751 │ ArrayOfByteUTF8String
  939055 │ StringBuilderChar
  907194 │ ArrayOfByteUTF8Const
  841963 │ StringBuilderCodePoint
  606465 │ MatcherReplace
  501555 │ StringReplaceAll
  381185 │ ArrayOfByteWindows1251

En forma de gráficos: (fuente: greycat.ru )Varias cadenas, concentración del 1%

Es muy difícil para mí decidir quién proporcionó la mejor respuesta, pero dada la aplicación del mundo real que mejor solución fue dada / inspirada por Ed Staub, creo que sería justo marcar su respuesta. Gracias a todos los que participaron en esto, su aporte fue muy útil e invaluable. Siéntase libre de ejecutar el conjunto de pruebas en su caja y proponer soluciones aún mejores (solución JNI funcional, ¿alguien?).

Referencias

Gato gris
fuente
21
"Esta pregunta muestra un esfuerzo de investigación" - hmm ... sí, pase. +1
Gustav Barkefors
7
StringBuilderserá un poco más rápido que StringBufferya que no está sincronizado, solo menciono esto porque etiquetó estomicro-optimization
2
@Jarrod Roberson: ok, hagamos que todos los campos de solo lectura sean finales y extraigamos s.length()también del forciclo :-)
inicio
3
Algunos caracteres debajo del espacio se pueden imprimir, por ejemplo, \ty \n. Muchos caracteres por encima de 127 no se pueden imprimir en su juego de caracteres.
Peter Lawrey
1
¿Iniciaste el búfer de cadena con una capacidad de s.length()?
monstruo del trinquete

Respuestas:

11

Si es razonable incrustar este método en una clase que no se comparte entre subprocesos, puede reutilizar el búfer:

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);

etc ...

Esta es una gran victoria: aproximadamente un 20%, según entiendo el mejor caso actual.

Si esto se va a usar en cadenas potencialmente grandes y la "pérdida" de memoria es una preocupación, se puede usar una referencia débil.

Ed Staub
fuente
¡Gran idea! Hasta ahora, ha llevado los recuentos a 3471017 cadenas por segundo, es decir, una mejora del + 12% con respecto a la mejor versión anterior.
GreyCat
25

el uso de una matriz de caracteres podría funcionar un poco mejor

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);

y evité repetidas llamadas a s.length();

otra microoptimización que podría funcionar es

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);
monstruo del trinquete
fuente
1
¡Gracias! Su versión rinde 3105590 cadenas / s, ¡una mejora enorme!
GreyCat
newLen++;: ¿qué pasa con el preincremento ++newLen;? - ( ++jen el bucle también). Eche un vistazo aquí: stackoverflow.com/questions/1546981/…
Thomas
Agregar finala este algoritmo y usar oldChars[newLen++]( ++newLenes un error, ¡la cadena completa estaría desviada en 1!) No produce ganancias de rendimiento medibles (es decir, obtengo diferencias de ± 2..3%, que son comparables a las diferencias de diferentes ejecuciones)
GreyCat
@grey Hice otra versión con algunas otras optimizaciones
fanático del trinquete
2
¡Hmm! ¡Esa es una idea brillante! El 99,9% de las cadenas en mi entorno de producción no requerirán realmente la eliminación; puedo mejorarlo aún más para eliminar incluso la primera char[]asignación y devolver la cadena como está, si no se realizó ninguna eliminación.
GreyCat
11

Bueno, he superado el mejor método actual (la solución de Freak con la matriz preasignada) en aproximadamente un 30% según mis medidas. ¿Cómo? Vendiendo mi alma.

Como estoy seguro de que todos los que han seguido la discusión hasta ahora saben que esto viola prácticamente cualquier principio básico de programación, pero bueno. De todos modos, lo siguiente solo funciona si la matriz de caracteres utilizada de la cadena no se comparte entre otras cadenas; si lo hace, quien tenga que depurar esto tendrá todo el derecho de decidir matarte (sin llamadas a substring () y usar esto en cadenas literales esto debería funcionar ya que no veo por qué la JVM internará cadenas únicas leídas desde una fuente externa). Aunque no olvide asegurarse de que el código de referencia no lo haga, eso es extremadamente probable y obviamente ayudaría a la solución de reflexión.

De todos modos aquí vamos:

    // 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;
    }

Para mi cadena de prueba que obtiene 3477148.18ops/sfrente 2616120.89ops/sa la variante anterior. Estoy bastante seguro de que la única forma de superar eso podría ser escribirlo en C (aunque probablemente no) o algún enfoque completamente diferente en el que nadie haya pensado hasta ahora. Aunque no estoy absolutamente seguro de si el tiempo es estable en diferentes plataformas, produce resultados confiables en mi caja (Java7, Win7 x64) al menos.

Voo
fuente
Gracias por la solución, consulte la actualización de la pregunta: publiqué mi marco de prueba y agregué 3 resultados de ejecución de prueba para 17 algoritmos. Su algoritmo siempre está en la parte superior, pero cambia el estado interno de Java String, rompiendo así el contrato de "cadena inmutable" => sería bastante difícil usarlo en aplicaciones del mundo real. En cuanto a las pruebas, sí, es el mejor resultado, pero supongo que lo anunciaré como una nominación separada :)
GreyCat
3
@GreyCat Sí, ciertamente tiene algunas condiciones importantes y, honestamente, solo lo escribí porque estoy bastante seguro de que no hay una forma notable de mejorar aún más su mejor solución actual. Hay situaciones en las que estoy seguro de que funcionará bien (sin subcadena o llamadas internas antes de eliminarlo), pero eso se debe al conocimiento sobre una versión actual de Hotspot (es decir, afaik, no internará cadenas leídas desde IO - no lo haría) t sea especialmente útil). Puede ser útil si uno realmente necesita esos x% adicionales, pero por lo demás es más una línea de base para ver cuánto puede mejorar todavía;)
Voo
1
Aunque intenté probar una versión de JNI si encuentro tiempo, nunca la usé hasta ahora, así que sería interesante. Pero estoy bastante seguro de que será más lento debido a la sobrecarga adicional de llamadas (las cadenas son demasiado pequeñas) y al hecho de que el JIT no debería tener tantas dificultades para optimizar las funciones. Simplemente no lo use new String()en caso de que su cadena no haya cambiado, pero creo que ya lo tiene.
Voo
Ya he intentado hacer exactamente lo mismo en C puro y, bueno, realmente no muestra mucha mejora con respecto a su versión basada en reflejos. La versión C se ejecuta algo así como un + 5..10% más rápido, no realmente tan bien - pensé que sería al menos como 1.5x-1.7x ...
GreyCat
2

Puede dividir la tarea en varias subtareas paralelas, según la cantidad del procesador.

umbr
fuente
Sí, también lo pensé, pero no producirá ningún aumento de rendimiento en mi situación: este algoritmo de eliminación se llamaría en un sistema ya masivamente paralelo.
GreyCat
2
Y, además, podría suponer que bifurcar algunos subprocesos para procesarlos por cada cadena de 50-100 bytes sería una exageración enorme.
GreyCat
Sí, bifurcar hilos para cada cuerda pequeña no es una buena idea. Pero el equilibrador de carga podría mejorar el rendimiento. Por cierto, ¿probó el rendimiento con StringBuilder en lugar de StringBuffer, que tiene fallas de rendimiento porque está sincronizado?
umbr
Mi configuración de producción genera varios procesos separados y utiliza tantas CPU y núcleos paralelos como sea posible, por lo que puedo usarlo libremente en StringBuildertodas partes sin ningún problema.
GreyCat
2

Era tan libre y escribí un pequeño punto de referencia para diferentes algoritmos. No es perfecto, pero tomo el mínimo de 1000 ejecuciones de un algoritmo dado 10000 veces en una cadena aleatoria (con aproximadamente 32/200% no imprimibles de forma predeterminada). Eso debería ocuparse de cosas como GC, inicialización, etc., no hay tanta sobrecarga que ningún algoritmo debería tener al menos una ejecución sin muchos obstáculos.

No está especialmente bien documentado, pero bueno. Aquí vamos : incluí tanto los algoritmos de Ratchet Freak como la versión básica. En este momento, inicializo aleatoriamente una cadena de 200 caracteres de longitud con caracteres distribuidos uniformemente en el rango [0, 200).

Voo
fuente
+1 por el esfuerzo, pero debería haberme preguntado, ya tengo una suite de evaluación comparativa similar, ahí es donde estaba probando mis algoritmos;)
GreyCat
@GreyCat Bueno, podría haberlo hecho, pero simplemente juntar eso (fuera del código existente de todos modos) fue probablemente más rápido;)
Voo
1

Adicto al rendimiento de Java de bajo nivel de IANA, pero ¿ha intentado desenrollar su bucle principal ? Parece que podría permitir que algunas CPU realicen comprobaciones en paralelo.

Además, esto tiene algunas ideas divertidas para optimizaciones.

Ryan Ransford
fuente
Dudo que se pueda desenrollar aquí, ya que hay (a) dependencias en los siguientes pasos del algoritmo en los pasos anteriores, (b) ni siquiera he oído hablar de nadie que haya desenrollado un bucle manual en Java que produzca resultados estelares; JIT generalmente hace un buen trabajo al desenrollar lo que considere adecuado para la tarea. Gracias por la sugerencia y un enlace, aunque :)
GreyCat
0

¿Por qué usar el nombre del juego de caracteres "utf-8" directamente produce un mejor rendimiento que usar el juego de caracteres constante estático preasignado Charset.forName ("utf-8")?

Si te refieres a String#getBytes("utf-8")etc .: Esto no debería ser más rápido, excepto por un mejor almacenamiento en caché, ya que Charset.forName("utf-8")se usa internamente, si el juego de caracteres no está en caché.

Una cosa podría ser que esté usando diferentes conjuntos de caracteres (o tal vez parte de su código lo haga de forma transparente) pero el conjunto de caracteres almacenado en caché StringCodingno cambia.

Thomas
fuente