يستمر هذا المنشور في مناقشة تحسينات الأداء التي يمكن أن تتحقق إذا لم تكن مختلفة. الجزء السابق حول StringBuilder
هنا .
هنا نلقي نظرة على العديد من "التحسينات" التي تم رفضها بسبب عدم فهم التفاصيل الدقيقة لمواصفات اللغة ، وحالات الركن غير الواضحة ، وأسباب أخرى. دعنا نذهب!
عندما لا شيء ينذر المتاعب
أعتقد أن كل واحد منا يعمل مع الأساليب Collections.emptySet()
/ Collections.emptyList()
. هذه طرق مفيدة للغاية تسمح لك بإرجاع مجموعة ثابتة غير قابلة للتغيير دون إنشاء كائن جديد. EmptyList
داخل فئة EmptyList
سنرى هذا:
private static class EmptyList<E> { public Iterator<E> iterator() { return emptyIterator(); } public Object[] toArray() { return new Object[0]; } }
رؤية إمكانات قوية للتحسين؟ إرجاع الأسلوب EmptyList.iterator()
مكرر فارغ من التواجد ، لماذا لا تجعل نفس الخدعة مع أذنيك للصفيف الذي تم إرجاعه بواسطة الأسلوب toArray()
؟
بمعنى آخر ، يجب أن تقوم الطريقة دائمًا بإرجاع صفيف جديد .
ستقول: "إنه غير قابل للتغيير! ما الخطأ الذي يمكن أن يحدث!"
يمكن فقط للخبراء ذوي الخبرة الإجابة على هذا السؤال:
- من المسؤول؟
- الخبراء المسؤولون بول ساندوز وتاجر فالييف
إجابة الخبراءhttp://mail.openjdk.java.net/pipermail/core-libs-dev/2017- سبتمبر 1/49171.html
لاحظ أيضًا أن هذا يغير السلوك المرئي. إي. قد تتم مزامنة شخص ما على كائن الصفيف الذي يتم إرجاعه بواسطة استدعاء toArray ، لذلك قد يتسبب هذا التغيير في مشاركة قفل غير مرغوب فيها.
بمجرد اقتراح تحسين مشابه: لإرجاع EMPTY_LIST من Arrays.asList () عندما يكون للصفيف المتوفر طول صفري. تم رفضه لنفس السبب [1].
http://mail.openjdk.java.net/pipermail/core-libs-dev/2015- سبتمبر 1/35197.html
بالمناسبة ، ربما يكون من المنطقي إذن أن يتحقق Arrays.asList من طول الصفيف مثل:
public static <T> List<T> asList(T... a) { if(a.length == 0) return Collections.emptyList(); return new ArrayList<>(a); }
تبدو معقولة ، أليس كذلك؟ لماذا إنشاء قائمة جديدة لصفيف فارغ إذا كنت تستطيع أن تأخذ قائمة جاهزة مجانًا؟
هناك سبب لعدم القيام بذلك. في الوقت الحالي ، لا تحدد Arrays.asList قيودًا على هوية القائمة التي تم إرجاعها. مضيفا التحسين الجزئي سيغير ذلك. إنها حالة حافة وحالة استخدام مشكوك فيها أيضًا ، ولكن بالنظر إلى أنني سأترك الأشياء بشكل متحفظ كما هي.
من المحتمل أن يتسبب هذا البيان في حيرة:
إي. قد تتم مزامنة شخص ما على كائن الصفيف الذي يتم إرجاعه بواسطة استدعاء toArray ، لذلك قد يتسبب هذا التغيير في مشاركة قفل غير مرغوب فيها.
ستقول: "من في عقلهم الصحيح سيتم مزامنة على مجموعة (!) عاد (!) من المجموعة!؟"
لا يبدو هذا أمرًا معقولًا ، لكن اللغة توفر مثل هذه الفرصة ، مما يعني أن هناك احتمالًا لأن مستخدمًا معينًا سيفعل ذلك (أو حتى قام بذلك بالفعل). بعد ذلك ، سيغير التغيير المقترح في أفضل الأحوال سلوك الكود ، وفي أسوأ الأحوال ، سيؤدي ذلك إلى تعطل المزامنة (انتقل لاحقًا ، امسحها). المخاطرة غير مبررة ، والمكسب المتوقع ضئيل للغاية لدرجة أنه من الأفضل ترك كل شيء كما هو.
بشكل عام ، كانت القدرة على المزامنة على أي كائن ، kmk ، هي خطأ مطوري اللغة. أولاً ، يحتوي رأس كل كائن على بنية مسؤولة عن التزامن ، وثانياً ، نجد أنفسنا في الموقف الموصوف أعلاه عندما لا يمكن إرجاع كائن يبدو غير قابل للتغيير عدة مرات ، حيث يمكن مزامنته عليه.
المعنوية لهذا الخرافة هي: المواصفات والتوافق مع الإصدارات السابقة هي بقرات مقدسة من جافا. لا تحاول حتى التعدي عليها: يطلق النار على الحارس دون سابق إنذار.
جرب ، يحاول ...
هناك العديد من الفئات المستندة إلى الصفيف في JDK مرة واحدة ، وكلها تقوم بتطبيق الأساليب List.indexOf()
و List.lastIndexOf()
:
- java.util.ArrayList
- java.util.Arrays $ ArrayList
- java.util.Vector
- java.util.concurrent.CopyOnWriteArrayList
يتم تكرار رمز هذه الطرق في هذه الفئات من واحد إلى واحد تقريبًا. تقدم العديد من التطبيقات والأطر أيضًا حلولها لنفس المشكلة:
نتيجة لذلك ، لدينا شفرة طائشة تحتاج إلى تجميع (أحيانًا عدة مرات في بعض الأحيان) ، والتي تحدث في ReserverCodeCache ، والتي تحتاج إلى اختبار ، والتي تتجول ببساطة من فئة إلى أخرى دون أي تغييرات تقريبًا.
المطورين ، بدورهم ، مغرمون للغاية من كتابة شيء من هذا القبيل
int i = Arrays.asList(array).indexOf(obj);
أرغب في تقديم طرق مساعدة عامة في JDK ، واستخدامها في كل مكان ، كما هو مقترح . التصحيح بسيط مثل بنسات اثنين:
1) تطبيقات List.indexOf()
و List.lastIndexOf()
تنتقل إلى java.util.Arrays
2) بدلاً من ذلك ، يتم Arrays.indexOf()
و Arrays.lastIndexOf()
، على التوالي
يبدو أن ما يمكن أن يحدث الخطأ؟ المكسب من هذا النهج واضح على ما يبدو. لكن المقالة تدور حول الفشل ، لذلك فكر في ما قد يحدث.
- من المسؤول؟
- الخبراء المسؤولون مارتن بوشولز وبول ساندوز
IMHO ، مشدود قليلا ، ولكن مع ذلكمارتن بوشهولز:
Sergey ، أنا من النوع الذي أحتفظ به في جميع فئات التجميع هذه ، وقد أردت أيضًا في بعض الأحيان استخدام أساليب indexOf في Array.java. لكن:
عادة ما يتم تثبيط المصفوفات. أي أساليب ثابتة جديدة في المصفوفات (أو ، حيثما أريدها بالفعل ، على كائن المصفوفة نفسها! تتطلب تغيير لغة جافا!) ستواجه المقاومة.
لقد شعرنا بالأسف لدعمها بالقيم الخالية في المجموعات ، لذلك لا تدعمهم فصول المجموعة الأحدث مثل ArrayDeque.
متغير آخر قد يريد المستخدمون هو نوع مقارنة المساواة لاستخدام.
لقد شعرنا بالأسف لوجود ArrayList مع فهرس البدء الصفري - كان من الأفضل أن يكون هناك سلوك صفيف دائري من ArrayDeque في اليوم الأول.
رمز البحث في شرائح الصفيف صغير جدًا ، لذا لا توفر الكثير. من السهل ارتكاب خطأ بعيدًا عن الآخر ، ولكن هذا صحيح بالنسبة لواجهة برمجة تطبيقات Arrays أيضًا.
بول ساندوز:
لن أذهب إلى حد القول بأن الصفائف غير مشجعة ، أدورها بشكل إيجابي باعتبارها "تستخدم بحذر" لأنها قابلة للتغيير بشكل دائم ، ويمكن تحسينها بالتأكيد ، وسأكون سعيدًا جدًا برؤية الصفائف تنفذ مجموعة مشتركة واجهة العش ، قد نكون قادرين على إحراز بعض التقدم بعد أنواع القيم الرواسب.
ستتم مواجهة أي إضافات جديدة إلى Arrays ببعض المقاومة ، رغم ذلك ، على الأقل :-) إنها لا تضيف أبدًا طريقة واحدة أو طريقتين فقط ، ويريد الكثيرون الآخرون الانتقال أيضًا (جميع البدائل بالإضافة إلى بدائل النطاق). لذلك يجب أن تكون أي ميزة جديدة مفيدة بما فيه الكفاية ، وفي هذه الحالة لا أعتقد أن الفوائد قوية بما فيه الكفاية (مثل ضغط ذاكرة التخزين المؤقت المحتمل لشفرة التخفيض).
بول.
المراسلات: http://mail.openjdk.java.net/pipermail/core-libs-dev/2018-March/051968.html
إن مغزى هذه الحكاية هو ذلك: يمكن ذبح الرقعة المبدعة للمراجعة لمجرد أنها لن ترى أي قيمة خاصة فيها. حسنًا ، نعم ، هناك كود مكرر ، لكنه لا يزعج أي شخص ، لذا دعه يعيش.
تحسينات ل ArrayList؟ لدي لهم
الدراجة التصحيح ليس لي ، سأقوم فقط بنشره لكي تفكر فيه. تم التعبير عن الاقتراح نفسه هنا ، ويبدو جذابًا للغاية. انظر لنفسك:
بالعين المجردة ، فإن الاقتراح منطقي للغاية. يمكنك قياس الأداء باستخدام معيار بسيط:
@State(Scope.Benchmark) public class ArrayListBenchmark { @State(Scope.Benchmark) public static class Data { @Param({"10", "100", "1000", "10000"}) public int size; ArrayList<Integer> arrayRandom = new ArrayList<Integer>(size); @Setup(Level.Invocation) public void initArrayList() { Random rand = new Random(); rand.setSeed(System.currentTimeMillis());
ملخص:
Benchmark (size) Mode Cnt Score Error Units construct_new_array_list 10 thrpt 25 388.212 ± 23.110 ops/s construct_new_array_list 100 thrpt 25 90.208 ± 7.995 ops/s construct_new_array_list 1000 thrpt 25 23.289 ± 1.687 ops/s construct_new_array_list 10000 thrpt 25 7.659 ± 0.560 ops/s construct_new_array_list 10 thrpt 25 562.678 ± 37.370 ops/s construct_new_array_list 100 thrpt 25 119.791 ± 13.232 ops/s construct_new_array_list 1000 thrpt 25 33.811 ± 3.812 ops/s construct_new_array_list 10000 thrpt 25 10.889 ± 0.564 ops/s
ليس سيئا على الإطلاق لمثل هذا التغيير البسيط. الشيء الرئيسي هو أنه لا يبدو أن هناك الصيد. بصراحة إنشاء مجموعة ، نسخ البيانات بصدق ولا تنسى عن الحجم. الآن يجب عليهم بالتأكيد قبول التصحيح!
ولكن كان هناكمارتن بوشهولز:
لا شك في أنه يمكننا تحسين حالة ArrayList -> ArrayList ، ولكن ماذا عن جميع تطبيقات المجموعة الأخرى؟ ArrayDeque و CopyOnWriteArrayList تتبادر إلى الذهن.
ArrayList هي فئة شائعة الاستخدام لصنع نسخ من المجموعات. أين تتوقف؟
يمكن أن تقرر فئة فرعية مرضية من ArrayList عدم تخزين العناصر في صفيف الدعم ، مع الكسر التالي.
ربما يكون الحل المبارك لمشكلة نسخ القائمة هو List.copyOf https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/List.html#copyOf(java .util.Collection ) التي قد تفعل التحسين الذي تأمل فيه.
آلان باتيمان
قائمة ArrayList ليست نهائية ، لذا فمن الممكن أن يقوم شخص ما بتوسيعه لاستخدام شيء آخر غير elementData. قد يكون أكثر أمانًا استخدام هوية الفئة بدلاً من مثيلها.
لا شيء يمنعني من الانفصال عن ArrayList
، وتخزين البيانات في قائمة مرتبطة. ثم c instanceof ArrayList
سيعيد الحقيقة ، سنصل إلى منطقة النسخ وسنقع بأمان.
إن مغزى هذا الخرافة هو: التغيير المحتمل في السلوك قد يكون سبب الفشل. بعبارة أخرى ، يجب على المرء أن يضع في الاعتبار احتمال حدوث أكبر تغيير سخيف ، إذا كان مسموحًا به عن طريق اللغة. ونعم ، كان يمكن أن ArrayList
إذا كان ArrayList
قد أعلن final
من البداية.
مواصفات مرة أخرى
أثناء تصحيح أخطاء طلبي ، وقعت بطريق الخطأ في أحشاء Spring ووجدت الكود التالي:
لحسن الحظ ، java.lang.reflect.Constructor.getParameterTypes()
انتقلت إلى الكود أقل قليلاً ووجدت رمزًا جميلًا:
@Override public Class<?>[] getParameterTypes() { return parameterTypes.clone(); } public int getParameterCount() { return parameterTypes.length; }
انت ترى نعم إذا احتجنا إلى معرفة عدد وسيطات المُنشئ / الطريقة ، فما عليك سوى الاتصال بـ java.lang.reflect.Method.getParameterCount()
والاستغناء عن نسخ الصفيف. تحقق مما إذا كانت اللعبة تستحق الشمعة في أبسط الحالات ، حيث لا تحتوي الطريقة على معلمات:
@State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class MethodToStringBenchmark { private Method method; @Setup public void setup() throws Exception { method = getClass().getMethod("toString"); } @Benchmark public int getParameterCount() { return method.getParameterCount(); } @Benchmark public int getParameterTypes() { return method.getParameterTypes().length; } }
على جهازي ومع JDK 11 ، يبدو الأمر كما يلي:
Benchmark Mode Cnt Score Error Units getParameterCount avgt 25 2,528 ± 0,085 ns/op getParameterCount:·gc.alloc.rate avgt 25 ≈ 10⁻⁴ MB/sec getParameterCount:·gc.alloc.rate.norm avgt 25 ≈ 10⁻⁷ B/op getParameterCount:·gc.count avgt 25 ≈ 0 counts getParameterTypes avgt 25 7,299 ± 0,410 ns/op getParameterTypes:·gc.alloc.rate avgt 25 1999,454 ± 89,929 MB/sec getParameterTypes:·gc.alloc.rate.norm avgt 25 16,000 ± 0,001 B/op getParameterTypes:·gc.churn.G1_Eden_Space avgt 25 2003,360 ± 91,537 MB/sec getParameterTypes:·gc.churn.G1_Eden_Space.norm avgt 25 16,030 ± 0,045 B/op getParameterTypes:·gc.churn.G1_Old_Gen avgt 25 0,004 ± 0,001 MB/sec getParameterTypes:·gc.churn.G1_Old_Gen.norm avgt 25 ≈ 10⁻⁵ B/op getParameterTypes:·gc.count avgt 25 2380,000 counts getParameterTypes:·gc.time avgt 25 1325,000 ms
ماذا يمكننا أن نفعل حيال ذلك؟ يمكننا البحث عن استخدام antipattern Method.getParameterTypes().length
في JDK (على الأقل في java.base
) واستبداله حيث يكون منطقيًا:
java.lang.invoke.MethodHandleProxies

java.util.concurrent.ForkJoinTask

java.lang.reflect.Executable

sun.reflect.annotation.AnnotationType

تم إرسال التصحيح مع رسالة تغطية .
فجأة اتضح أنه كانت هناك مهمة مماثلة لعدة سنوات ، وحتى التغييرات التي تم إعدادها لها. لاحظت التعليقات زيادة في الأداء اللائق لمثل هذه التغييرات البسيطة. ومع ذلك ، يتم تنظيف كل من هم وبلدي التصحيح حتى الآن والكذب بلا حراك. لماذا؟ ربما لأن المطورين مشغولون جدًا بالمزيد من الأشياء الضرورية وهم أغبياء لا يضعون أيديهم عليها.
إن مغزى هذه الحكاية هو: يمكن أن تتجمد تغييراتك العبقرية ببساطة بسبب نقص العمال.
لكن هذه ليست النهاية! أثناء مناقشة عقلانية الاستبدال الموصوف في مشاريع أخرى ، قدم الرفاق الأكثر خبرة اقتراحًا Method.getParameterTypes().length -> Method.getParameterCount()
: ربما لا ينبغي عليك القيام باستبدال Method.getParameterTypes().length -> Method.getParameterCount()
بأيديك ، ولكن هل تعهد بذلك إلى المترجم؟ هل هذا ممكن وسيكون "قانوني"؟
دعنا نحاول التحقق من استخدام الاختبار:
@Test void arrayClone() { final Object[] objects = new Object[3]; objects[0] = "azaza"; objects[1] = 365; objects[2] = 9876L; final Object[] clone = objects.clone(); assertEquals(objects.length, clone.length); assertSame(objects[0], clone[0]); assertSame(objects[1], clone[1]); assertSame(objects[2], clone[2]); }
الذي يمر ، والذي يوضح أنه إذا لم تترك الصفيف المستنسخة النطاق ، فيمكن حذفه ، لأنه يمكن الحصول على الوصول إلى أي عنصر من خلاياه أو حقل length
من الأصل.
يمكن JDK القيام بذلك؟ نتحقق من:
@State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class ArrayAllocationEliminationBenchmark { private int length = 10; @Benchmark public int baseline() { return new int[length].length; } @Benchmark public int baselineClone() { return new int[length].clone().length; } }
إخراج JDK 13:
Benchmark Mode Cnt Score Error Units baseline avgt 50 6,135 ± 0,140 ns/op baseline:·gc.alloc.rate.norm avgt 50 56,000 ± 0,001 B/op clone avgt 50 18,359 ± 0,619 ns/op clone:·gc.alloc.rate.norm avgt 50 112,000 ± 0,001 B/op
اتضح أن openjdk لا يعرف كيفية إلقاء new int[length]
، على عكس الكأس ، الكالينجيون:
Benchmark Mode Cnt Score Error Units baseline avgt 25 2,470 ± 0,156 ns/op baseline:·gc.alloc.rate.norm avgt 25 0,005 ± 0,008 B/op lone avgt 25 13,086 ± 1,059 ns/op lone:·gc.alloc.rate.norm avgt 25 112,113 ± 0,115 B/op
اتضح أنه يمكنك تعديل برنامج التحويل البرمجي openjdk الذي يعمل على تحسين المحول البرمجي قليلاً حتى يتمكن من فعل ما يمكن أن يفعله Grail. نظرًا لأنه لا يمكن لأي شخص الدخول في إعلان إيجابي في كود VM وتقديم شيء ذي معنى ، فقد قصرت نفسي على رسالة ذكرت فيها ملاحظاتي.
اتضح ، وهناك العديد من التفاصيل الدقيقة. يشير فلاديمير إيفانوف إلى :
بالنظر إلى أنه لا توجد طريقة لزراعة / تقليص صفيفات Java ،
التحويل "cloned_array.length => original_array.length" صحيح
بغض النظر عما إذا كان البديل المستنسخ يهرب أم لا.
علاوة على ذلك ، فإن التحول موجود بالفعل:
http://hg.openjdk.java.net/jdk/jdk/file/tip/src/hotspot/share/opto/memnode.cpp#l2388
لم أتطرق إلى المعايير التي ذكرتها ، لكن يبدو الأمر كذلك
الوصول إلى cloned_array.length ليس هو السبب وراء استمرار وجود مجموعة cloned
هناك.
فيما يتعلق بأفكارك الأخرى ، إعادة توجيه الوصول من مثيل cloned إلى
الأصلي هو إشكالية (في الحالة العامة) لأن المترجم يجب أن يثبت
لم تكن هناك تغييرات في كلا الإصدارين ووصول مفهرسة يجعلها متساوية
أصعب. وتسبب نقاط الأمان مشاكل أيضًا (لإعادة التجسيد المادي).
لكنني أوافق على أنه سيكون من الجيد تغطية (على الأقل) حالات بسيطة من
نسخ دفاعي.
وهذا يعني ، يبدو أن قرع استنساخ ممكن ، وليس صعبًا بشكل خاص. ولكن مع التحويل
int len = new int[arrayLength].length;
->
int len = arrayLength;
الصعوبات تنشأ:
لا نحذف تخصيصات الصفيف التي ليس لها طول معروف
لأنها قد تسبب استثناء NegativeArraySize. في هذه الحالة نحن
يجب أن تكون قادرة على إثبات أن طول إيجابي على الرغم من.
على أي حال - لدي تصحيح انتهى تقريبًا يستبدل صفيف غير مستخدم
التخصيصات مع حارس مناسب.
بمعنى آخر ، لا يمكنك فقط إخراج إنشاء صفيف ورميها ، لأنه وفقًا للمواصفات ، يجب أن يرمي التنفيذ NegativeArraySizeException
ولا يوجد شيء يمكننا القيام به حيال ذلك:
@Test void arrayWithNwgativeSize() { int length = 0; try { int newLen = -3; length = new Object[newLen].length;
لماذا كانت الكأس قادرة؟ أعتقد أن السبب في ذلك هو أن قيمة حقل length
في المعيار أعلاه كانت ثابتة وتساوي دائمًا 10 ، مما سمح للمبرمج باستنتاج أن التحقق من قيمة سالبة ليس ضروريًا ، مما يعني أنه يمكن إزالته مع إنشاء المصفوفة نفسها. صحيح في التعليقات إذا ارتكبت خطأ.
هذا كل شيء لهذا اليوم :) أضف أمثلةك في التعليقات ، وسوف نفهمها.