【问题标题】:Custom "hash table" implementation: Why is it so slow? (bytecode generation)自定义“哈希表”实现:为什么这么慢? (字节码生成)
【发布时间】:2014-05-18 21:10:16
【问题描述】:

今天,我回答了一些Java初学者的普通question。过了一会儿,我觉得认真对待他的问题会很有趣,所以我实现了他想要的。

我为运行时类生成创建了一个简单的代码。大部分代码取自模板,唯一可能的更改是声明一些字段。生成的代码可以写成:

public class Container implements Storage {
    private int    foo; // user defined (runtime generated)
    private Object boo; // user defined (runtime generated)

    public Container() {
        super();
    }
}

然后使用自定义 ClassLoader 将生成的 Class 文件加载到 JVM。

然后我实现了“静态哈希表”之类的东西。程序员输入所有可能的键,然后生成一个类(其中每个键都作为一个字段)。在我们拥有此类实例的那一刻,我们还可以使用反射保存或读取这些生成的字段。

这是一个完整的代码:

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Random;

class ClassGenerator extends ClassLoader {
    private ArrayList<FieldInfo> fields = new ArrayList<ClassGenerator.FieldInfo>();

    public static class FieldInfo {
        public final String   name;
        public final Class<?> type;

        public FieldInfo(String name, Class<?> type) {
            this.name = name;
            this.type = type;
        }
    }

    private static class ComponentTypeInfo {
        private final Class<?> type;
        private final int      arrayDimensions;

        public ComponentTypeInfo(Class<?> type, int arrayDimensions) {
            this.type            = type;
            this.arrayDimensions = arrayDimensions;
        }
    }

    private static ComponentTypeInfo getComponentType(Class<?> type) {
        Class<?> tmp   = type;
        int      array = 0;

        while (tmp.isArray()) {
            tmp = tmp.getComponentType();
            array++;
        }

        return new ComponentTypeInfo(tmp, array);
    }

    public static String getFieldDescriptor(Class<?> type) {
        ComponentTypeInfo componentTypeInfo  = getComponentType(type);
        Class<?>          componentTypeClass = componentTypeInfo.type;
        int               componentTypeArray = componentTypeInfo.arrayDimensions;
        String            result             = "";

        for (int i = 0; i < componentTypeArray; i++) {
            result += "[";
        }

        if (componentTypeClass.isPrimitive()) {
            if (componentTypeClass.equals(byte.class))    return result + "B";
            if (componentTypeClass.equals(char.class))    return result + "C";
            if (componentTypeClass.equals(double.class))  return result + "D";
            if (componentTypeClass.equals(float.class))   return result + "F";
            if (componentTypeClass.equals(int.class))     return result + "I";
            if (componentTypeClass.equals(long.class))    return result + "J";
            if (componentTypeClass.equals(short.class))   return result + "S";
            if (componentTypeClass.equals(boolean.class)) return result + "Z";

            throw new RuntimeException("Unknown primitive type.");
        } else {
            return result + "L" + componentTypeClass.getCanonicalName().replace('.', '/') + ";";
        }
    }

    public void addField(String name, Class<?> type) {
        this.fields.add(new FieldInfo(name, type));
    }

    private Class<?> defineClass(byte[] data) {
        return this.defineClass(null, data, 0, data.length);
    }

    private byte[] toBytes(short[] data) {
        byte[] result = new byte[data.length];

        for (int i = 0; i < data.length; i++) {
            result[i] = (byte) data[i];
        }

        return result;
    }

    private byte[] toBytes(short value) {
        return new byte[]{(byte) (value >> 8), (byte) (value & 0xFF)};
    }

    public Class<?> getResult() throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        outputStream.write(toBytes(new short[]{
            0xCA, 0xFE, 0xBA, 0xBE, // magic
            0x00, 0x00, 0x00, 0x33, // version
        }));

        // constantPoolCount
        outputStream.write(toBytes((short) (0x0C + (this.fields.size() * 2))));

        // constantPool
        outputStream.write(toBytes(new short[]{
            0x01, 0x00, 0x09, 'C', 'o', 'n', 't', 'a', 'i', 'n', 'e', 'r',
            0x01, 0x00, 0x10, 'j', 'a', 'v', 'a', '/', 'l', 'a', 'n', 'g', '/', 'O', 'b', 'j', 'e', 'c', 't',
            0x01, 0x00, 0x06, '<', 'i', 'n', 'i', 't', '>',
            0x01, 0x00, 0x03, '(', ')', 'V',
            0x01, 0x00, 0x04, 'C', 'o', 'd', 'e',

            0x07, 0x00, 0x01, // class Container
            0x07, 0x00, 0x02, // class java/lang/Object

            0x0C, 0x00, 0x03, 0x00, 0x04, // nameAndType
            0x0A, 0x00, 0x07, 0x00, 0x08, // methodRef

            0x01, 0x00, 0x07, 'S', 't', 'o', 'r', 'a', 'g', 'e',
            0x07, 0x00, 0x0A, // class Storage
        }));

        for (FieldInfo field : fields) {
            String name            = field.name;
            String descriptor      = getFieldDescriptor(field.type);

            byte[] nameBytes       = name.getBytes();
            byte[] descriptorBytes = descriptor.getBytes();

            outputStream.write(0x01);
            outputStream.write(toBytes((short) nameBytes.length));
            outputStream.write(nameBytes);

            outputStream.write(0x01);
            outputStream.write(toBytes((short) descriptorBytes.length));
            outputStream.write(descriptorBytes);
        }

        outputStream.write(toBytes(new short[]{
            0x00, 0x01, // accessFlags,
            0x00, 0x06, // thisClass
            0x00, 0x07, // superClass
            0x00, 0x01, // interfacesCount
            0x00, 0x0B  // interface Storage 
        }));

        // fields
        outputStream.write(toBytes((short) this.fields.size()));
        for (int i = 0; i < fields.size(); i++) {
            outputStream.write(new byte[]{0x00, 0x01});
            outputStream.write(toBytes((short) (12 + 2 * i)));
            outputStream.write(toBytes((short) (12 + 2 * i + 1)));
            outputStream.write(new byte[]{0x00, 0x00});
        }

        // methods and rest of the class file 
        outputStream.write(toBytes(new short[]{
            0x00, 0x01,                     // methodsCount
                // void <init>
                0x00, 0x01,                 // accessFlags
                0x00, 0x03,                 // nameIndex
                0x00, 0x04,                 // descriptorIndex,
                0x00, 0x01,                 // attributesCount
                    0x00, 0x05,             // nameIndex
                    0x00, 0x00, 0x00, 0x11, // length
                    0x00, 0x01,             // maxStack
                    0x00, 0x01,             // maxLocals,
                    0x00, 0x00, 0x00, 0x05, // codeLength
                        0x2A,               // aload_0
                        0xB7, 0x00, 0x09,   // invokespecial #9
                        0xB1,               // return
                    0x00, 0x00,             // exceptionTableLength
                    0x00, 0x00,             // attributesCount

            0x00, 0x00,                     // attributesCount
        }));

        return defineClass(outputStream.toByteArray());
    }
}

class SuperTable<T> {
    private Class<?> generatedClass = null;
    private Storage  container      = null;

    public SuperTable(String[] keys, Class<T> type) {
        ClassGenerator classGenerator = new ClassGenerator();

        for (String key : keys) {
            classGenerator.addField(key, type);
        }

        try {
            this.generatedClass = classGenerator.getResult();
            this.container      = (Storage) generatedClass.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void put(String name, Object value) {
        try {
            this.generatedClass.getDeclaredField(name).set(container, value);
        } catch (Exception e) {
            throw new RuntimeException("Such a field doesn't exist or is not accessible.");
        }
    }

    public Object get(String name) {
        try {
            return this.generatedClass.getDeclaredField(name).get(container);
        } catch (Exception e) {
            throw new RuntimeException("Such a field doesn't exist or is not accessible.");
        }
    }
}

public class Test {
    private static final String[] keys       = new String[(int) Math.pow(26, 3)];
    private static final Random   randomizer = new Random();

    static {
        int index = 0;

        for (char a = 'a'; a <= 'z'; a++) {
            for (char b = 'a'; b <= 'z'; b++) {
                for (char c = 'a'; c <= 'z'; c++) {
                    keys[index] = new String(new char[]{a, b, c});
                    index++;
                }
            }
        }
    }

    public static float test1(Hashtable<String, Integer> table, long count) {
        long time0 = System.currentTimeMillis();

        for (long i = 0; i < count; i++) {
            boolean step = randomizer.nextBoolean();
            String  key  = keys[randomizer.nextInt(keys.length)];

            if (step) {
                table.put(key, randomizer.nextInt());
            } else {
                table.get(key);
            }
        }

        return System.currentTimeMillis() - time0;
    }

    public static float test2(SuperTable<Integer> table, long count) {
        long time0 = System.currentTimeMillis();

        for (long i = 0; i < count; i++) {
            boolean step = randomizer.nextBoolean();
            String  key  = keys[randomizer.nextInt(keys.length)];

            if (step) {
                table.put(key, randomizer.nextInt());
            } else {
                table.get(key);
            }
        }

        return System.currentTimeMillis() - time0;
    }

    public static void main(String[] args) throws Exception {
        Hashtable<String, Integer> table  = new Hashtable<String, Integer>();
        SuperTable<Integer>        table2 = new SuperTable<Integer>(keys, Integer.class);

        long count = 500000;

        System.out.printf("Hashtable: %f ms\n", test1(table, count));
        System.out.printf("SuperTable: %f ms\n", test2(table2, count));
    }
}

它可以工作,但速度非常慢。我预计它会快一点,因为数据存储在由 JVM(使用本机代码)操作的字段中。我能想到的最严重的解释是反射非常慢。

为了清楚起见,无论如何我都不会使用它。事件如果实际上更快,代码是如此糟糕和不可维护,那将不值得。功能也非常有限(键必须是有效的字段名称等)。这看起来像是一个很酷的实验。

无论如何,有人知道为什么它比“普通”哈希表慢 100 倍吗?我猜这是由反思引起的,但我会感谢其他人的意见。

更新:正如 Antimony 和 NSF 所指出的,这确实是由反射引起的。我尝试以“正常”方式并使用反射设置一些静态字段。根据这个测试,反射慢了大约 280 倍。但我不知道为什么。

【问题讨论】:

    标签: java hashtable code-generation bytecode


    【解决方案1】:

    与常规代码相比,反射通常慢一个数量级,因为 JVM 无法执行某些优化:

    http://docs.oracle.com/javase/tutorial/reflect/index.html

    你的方法很有趣,但恐怕根本不实用。

    测试也可能是一个问题:注意两个测试用例一个接一个地执行,而第二个 GC 可能会启动。

    【讨论】:

    • 感谢您的回复。 GC 可能不是问题 - 我尝试执行两次,每次运行不同的测试,它提供了相同的结果。但你是对的,这可能是由它引起的。
    • 嗨,感谢您再次回复,它以正确的方式踢了我。我相信我已经找到了我正在寻找的答案:stackoverflow.com/a/22900490/2709026
    【解决方案2】:

    好的,我明白了。我认为方法 getDeclaredField 是本机的,JVM 将字段存储在某个哈希表中。在这种情况下,我的解决方案可能会非常快。

    但是,getDeclaredField 不是原生的。不知何故,它以数组的形式获取所有声明的字段,然后使用 searchFields 找到正确的字段。

    以下是 Oracle JDK 的摘录:

    private Field searchFields(Field[] fields, String name) {
        String internedName = name.intern();
        for (int i = 0; i < fields.length; i++) {
            if (fields[i].getName() == internedName) {
                return getReflectionFactory().copyField(fields[i]);
            }
        }
        return null;
    }
    

    据我们所见,它遍历 数组 并比较名称。

    现在它完全有道理。在上面的示例中,有 17 576 个字段。当我们假设一个字段通常位于中间某处时,它给了我们大约 8800 次迭代来定位一个字段。

    Field 的方法 setget 都不是原生的。在某些时候,它显然会落入本机代码中,但它比我预期的要晚得多。

    那么,我的代码到底做了什么?它没有使用一些 JVM 的内部哈希表(甚至可能不存在),而是实际上在至少一层上使用了一个普通数组。

    仅从这一点来看,甚至不考虑其他层,它一定非常慢 - 确实如此。

    致谢AntimonyNSF 以正确的方式踢球。

    【讨论】:

    • 很高兴知道这一点。编写此代码的人可能永远不会意识到,在某个时候有人会提出这样一个奇怪的想法,即在一个类中拥有数千个字段。
    【解决方案3】:

    首先,Java 中的标准反射真的很慢。但即使不是,我也不确定您为什么希望这段代码很快。

    想想如果 JIT 足够聪明,可以跨反射进行优化,JIT 将如何优化这段代码,并且它被编码为针对这种情况进行优化。优化它的最佳方法是构建一个以类名为键的哈希表,以便在后台查找每个字段。但此时,您刚刚创建了一个较慢版本的哈希表!这是最好的理想情况!

    使问题更复杂的是 JIT 旨在优化常见情况。没有一个头脑正常的人会这样做,所以它不太可能被优化。

    【讨论】:

    • 感谢您的回复。 JIT 似乎也不是问题。在完全禁用 JIT (-Xint) 的情况下,我得到 Hashtable 的值 66 和 SuperTable 的 7239。在执行时正确应用 JIT (-XX:CompileThreshold=0),我得到 Hashtable 的值 65 和 SuperTable 的 7073。所以 JIT 对 SuperTable 产生了巨大的影响。但也许我错过了什么?
    • 嗨,感谢您再次回复,它以正确的方式踢了我。我相信我已经找到了我正在寻找的答案:stackoverflow.com/a/22900490/2709026
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-12-31
    • 1970-01-01
    • 2015-12-04
    • 2013-03-24
    • 1970-01-01
    • 1970-01-01
    • 2011-03-27
    相关资源
    最近更新 更多