From 6afe7e8c55466d4d8e4d8f73a6dff7f845d4baf4 Mon Sep 17 00:00:00 2001 From: jamborm Date: Tue, 27 Apr 2010 10:07:47 +0000 Subject: [PATCH 1/1] 2010-04-27 Martin Jambor PR middle-end/43812 * ipa.c (dissolve_same_comdat_group_list): New function. (function_and_variable_visibility): Call dissolve_same_comdat_group_list when comdat group contains external or newly local nodes. * cgraphunit.c (verify_cgraph_node): Verify that same_comdat_group lists are circular and that they contain only DECL_ONE_ONLY nodes. * testsuite/g++.dg/ipa/pr43812.C: New test. git-svn-id: svn+ssh://gcc.gnu.org/svn/gcc/trunk@158777 138bc75d-0d04-0410-961f-82ee72b054a4 --- gcc/ChangeLog | 10 + gcc/cgraphunit.c | 26 ++ gcc/ipa.c | 616 ++++++------------------------------------------ gcc/testsuite/ChangeLog | 5 + 4 files changed, 114 insertions(+), 543 deletions(-) diff --git a/gcc/ChangeLog b/gcc/ChangeLog index 5402f874871..161bafbfd64 100644 --- a/gcc/ChangeLog +++ b/gcc/ChangeLog @@ -1,3 +1,13 @@ +2010-04-27 Martin Jambor + + PR middle-end/43812 + * ipa.c (dissolve_same_comdat_group_list): New function. + (function_and_variable_visibility): Call + dissolve_same_comdat_group_list when comdat group contains external or + newly local nodes. + * cgraphunit.c (verify_cgraph_node): Verify that same_comdat_group + lists are circular and that they contain only DECL_ONE_ONLY nodes. + 2010-04-27 Eric Botcazou * varasm.c (decode_addr_const): Handle special case of INDIRECT_REF. diff --git a/gcc/cgraphunit.c b/gcc/cgraphunit.c index fcb96b2044c..51b4732b45a 100644 --- a/gcc/cgraphunit.c +++ b/gcc/cgraphunit.c @@ -714,6 +714,32 @@ verify_cgraph_node (struct cgraph_node *node) error ("double linked list of clones corrupted"); error_found = true; } + if (node->same_comdat_group) + { + struct cgraph_node *n = node->same_comdat_group; + + if (!DECL_ONE_ONLY (node->decl)) + { + error ("non-DECL_ONE_ONLY node in a same_comdat_group list"); + error_found = true; + } + if (n == node) + { + error ("node is alone in a comdat group"); + error_found = true; + } + do + { + if (!n->same_comdat_group) + { + error ("same_comdat_group is not a circular list"); + error_found = true; + break; + } + n = n->same_comdat_group; + } + while (n != node); + } if (node->analyzed && gimple_has_body_p (node->decl) && !TREE_ASM_WRITTEN (node->decl) diff --git a/gcc/ipa.c b/gcc/ipa.c index abe5c9e5fe1..8295357cc0e 100644 --- a/gcc/ipa.c +++ b/gcc/ipa.c @@ -27,7 +27,6 @@ along with GCC; see the file COPYING3. If not see #include "timevar.h" #include "gimple.h" #include "ggc.h" -#include "flags.h" /* Fill array order with all nodes with output flag set in the reverse topological order. */ @@ -119,95 +118,6 @@ update_inlined_to_pointer (struct cgraph_node *node, struct cgraph_node *inlined } } -/* Add cgraph NODE to queue starting at FIRST. - - The queue is linked via AUX pointers and terminated by pointer to 1. - We enqueue nodes at two occasions: when we find them reachable or when we find - their bodies needed for further clonning. In the second case we mark them - by pointer to 2 after processing so they are re-queue when they become - reachable. */ - -static void -enqueue_cgraph_node (struct cgraph_node *node, struct cgraph_node **first) -{ - /* Node is still in queue; do nothing. */ - if (node->aux && node->aux != (void *) 2) - return; - /* Node was already processed as unreachable, re-enqueue - only if it became reachable now. */ - if (node->aux == (void *)2 && !node->reachable) - return; - node->aux = *first; - *first = node; -} - -/* Add varpool NODE to queue starting at FIRST. */ - -static void -enqueue_varpool_node (struct varpool_node *node, struct varpool_node **first) -{ - node->aux = *first; - *first = node; -} - -/* Process references. */ - -static void -process_references (struct ipa_ref_list *list, - struct cgraph_node **first, - struct varpool_node **first_varpool, - bool before_inlining_p) -{ - int i; - struct ipa_ref *ref; - for (i = 0; ipa_ref_list_reference_iterate (list, i, ref); i++) - { - if (ref->refered_type == IPA_REF_CGRAPH) - { - struct cgraph_node *node = ipa_ref_node (ref); - if (!node->reachable - && (!DECL_EXTERNAL (node->decl) - || before_inlining_p)) - { - node->reachable = true; - enqueue_cgraph_node (node, first); - } - } - else - { - struct varpool_node *node = ipa_ref_varpool_node (ref); - if (!node->needed) - { - varpool_mark_needed_node (node); - enqueue_varpool_node (node, first_varpool); - } - } - } -} - -/* Return true when function NODE can be removed from callgraph - if all direct calls are eliminated. */ - -static inline bool -varpool_can_remove_if_no_refs (struct varpool_node *node) -{ - return (!node->force_output && !node->used_from_other_partition - && (DECL_COMDAT (node->decl) || !node->externally_visible)); -} - -/* Return true when function can be marked local. */ - -static bool -cgraph_local_node_p (struct cgraph_node *node) -{ - return (cgraph_only_called_directly_p (node) - && node->analyzed - && !DECL_EXTERNAL (node->decl) - && !node->local.externally_visible - && !node->reachable_from_other_partition - && !node->in_other_partition); -} - /* Perform reachability analysis and reclaim all unreachable nodes. If BEFORE_INLINING_P is true this function is called before inlining decisions has been made. If BEFORE_INLINING_P is false this function also @@ -217,9 +127,8 @@ bool cgraph_remove_unreachable_nodes (bool before_inlining_p, FILE *file) { struct cgraph_node *first = (struct cgraph_node *) (void *) 1; - struct varpool_node *first_varpool = (struct varpool_node *) (void *) 1; + struct cgraph_node *processed = (struct cgraph_node *) (void *) 2; struct cgraph_node *node, *next; - struct varpool_node *vnode, *vnext; bool changed = false; #ifdef ENABLE_CHECKING @@ -230,17 +139,16 @@ cgraph_remove_unreachable_nodes (bool before_inlining_p, FILE *file) #ifdef ENABLE_CHECKING for (node = cgraph_nodes; node; node = node->next) gcc_assert (!node->aux); - for (vnode = varpool_nodes; vnode; vnode = vnode->next) - gcc_assert (!vnode->aux); #endif - varpool_reset_queue (); for (node = cgraph_nodes; node; node = node->next) - if (!cgraph_can_remove_if_no_direct_calls_and_refs_p (node) + if (!cgraph_can_remove_if_no_direct_calls_p (node) && ((!DECL_EXTERNAL (node->decl)) + || !node->analyzed || before_inlining_p)) { gcc_assert (!node->global.inlined_to); - enqueue_cgraph_node (node, &first); + node->aux = first; + first = node; node->reachable = true; } else @@ -248,104 +156,79 @@ cgraph_remove_unreachable_nodes (bool before_inlining_p, FILE *file) gcc_assert (!node->aux); node->reachable = false; } - for (vnode = varpool_nodes; vnode; vnode = vnode->next) - { - vnode->next_needed = NULL; - vnode->prev_needed = NULL; - if (!varpool_can_remove_if_no_refs (vnode)) - { - vnode->needed = false; - varpool_mark_needed_node (vnode); - enqueue_varpool_node (vnode, &first_varpool); - } - else - vnode->needed = false; - } /* Perform reachability analysis. As a special case do not consider extern inline functions not inlined as live because we won't output - them at all. - - We maintain two worklist, one for cgraph nodes other for varpools and - are finished once both are empty. */ - - while (first != (struct cgraph_node *) (void *) 1 - || first_varpool != (struct varpool_node *) (void *) 1) + them at all. */ + while (first != (void *) 1) { - if (first != (struct cgraph_node *) (void *) 1) - { - struct cgraph_edge *e; - node = first; - first = (struct cgraph_node *) first->aux; - if (!node->reachable) - node->aux = (void *)2; - - /* If we found this node reachable, first mark on the callees - reachable too, unless they are direct calls to extern inline functions - we decided to not inline. */ - if (node->reachable) - for (e = node->callees; e; e = e->next_callee) - if (!e->callee->reachable - && node->analyzed - && (!e->inline_failed || !e->callee->analyzed - || (!DECL_EXTERNAL (e->callee->decl)) - || before_inlining_p)) - { - e->callee->reachable = true; - enqueue_cgraph_node (e->callee, &first); - } - - /* If any function in a comdat group is reachable, force - all other functions in the same comdat group to be - also reachable. */ - if (node->same_comdat_group - && node->reachable - && !node->global.inlined_to) + struct cgraph_edge *e; + node = first; + first = (struct cgraph_node *) first->aux; + node->aux = processed; + + if (node->reachable) + for (e = node->callees; e; e = e->next_callee) + if (!e->callee->reachable + && node->analyzed + && (!e->inline_failed || !e->callee->analyzed + || (!DECL_EXTERNAL (e->callee->decl)) + || before_inlining_p)) { - for (next = node->same_comdat_group; - next != node; - next = next->same_comdat_group) - if (!next->reachable) - { - next->reachable = true; - enqueue_cgraph_node (next, &first); - } + bool prev_reachable = e->callee->reachable; + e->callee->reachable |= node->reachable; + if (!e->callee->aux + || (e->callee->aux == processed + && prev_reachable != e->callee->reachable)) + { + e->callee->aux = first; + first = e->callee; + } } - /* We can freely remove inline clones even if they are cloned, however if - function is clone of real clone, we must keep it around in order to - make materialize_clones produce function body with the changes - applied. */ - while (node->clone_of && !node->clone_of->aux && !gimple_has_body_p (node->decl)) + /* If any function in a comdat group is reachable, force + all other functions in the same comdat group to be + also reachable. */ + if (node->same_comdat_group + && node->reachable + && !node->global.inlined_to) + { + for (next = node->same_comdat_group; + next != node; + next = next->same_comdat_group) + if (!next->reachable) + { + next->aux = first; + first = next; + next->reachable = true; + } + } + + /* We can freely remove inline clones even if they are cloned, however if + function is clone of real clone, we must keep it around in order to + make materialize_clones produce function body with the changes + applied. */ + while (node->clone_of && !node->clone_of->aux && !gimple_has_body_p (node->decl)) + { + bool noninline = node->clone_of->decl != node->decl; + node = node->clone_of; + if (noninline) { - bool noninline = node->clone_of->decl != node->decl; - node = node->clone_of; - if (noninline && !node->reachable && !node->aux) - { - enqueue_cgraph_node (node, &first); - break; - } + node->aux = first; + first = node; + break; } - process_references (&node->ref_list, &first, &first_varpool, before_inlining_p); - } - if (first_varpool != (struct varpool_node *) (void *) 1) - { - vnode = first_varpool; - first_varpool = (struct varpool_node *)first_varpool->aux; - vnode->aux = NULL; - process_references (&vnode->ref_list, &first, &first_varpool, before_inlining_p); } } - /* Remove unreachable nodes. - - Completely unreachable functions can be fully removed from the callgraph. - Extern inline functions that we decided to not inline need to become unanalyzed nodes of - callgraph (so we still have edges to them). We remove function body then. - - Also we need to care functions that are unreachable but we need to keep them around - for later clonning. In this case we also turn them to unanalyzed nodes, but - keep the body around. */ + /* Remove unreachable nodes. Extern inline functions need special care; + Unreachable extern inline functions shall be removed. + Reachable extern inline functions we never inlined shall get their bodies + eliminated. + Reachable extern inline functions we sometimes inlined will be turned into + unanalyzed nodes so they look like for true extern functions to the rest + of code. Body of such functions is released via remove_node once the + inline clones are eliminated. */ for (node = cgraph_nodes; node; node = next) { next = node->next; @@ -368,7 +251,7 @@ cgraph_remove_unreachable_nodes (bool before_inlining_p, FILE *file) /* See if there is reachable caller. */ for (e = node->callers; e; e = e->next_caller) - if (e->caller->reachable) + if (e->caller->aux) break; /* If so, we need to keep node in the callgraph. */ @@ -421,61 +304,6 @@ cgraph_remove_unreachable_nodes (bool before_inlining_p, FILE *file) } node->aux = NULL; } - - if (file) - fprintf (file, "\n"); - - /* We must release unused extern inlines or sanity checking will fail. Rest of transformations - are undesirable at -O0 since we do not want to remove anything. */ - if (!optimize) - return changed; - - if (file) - fprintf (file, "Reclaiming variables:"); - for (vnode = varpool_nodes; vnode; vnode = vnext) - { - vnext = vnode->next; - if (!vnode->needed) - { - if (file) - fprintf (file, " %s", varpool_node_name (vnode)); - varpool_remove_node (vnode); - changed = true; - } - } - - /* Now update address_taken flags and try to promote functions to be local. */ - - if (file) - fprintf (file, "\nClearing address taken flags:"); - for (node = cgraph_nodes; node; node = node->next) - if (node->address_taken - && !node->reachable_from_other_partition) - { - int i; - struct ipa_ref *ref; - bool found = false; - for (i = 0; ipa_ref_list_refering_iterate (&node->ref_list, i, ref) - && !found; i++) - { - gcc_assert (ref->use == IPA_REF_ADDR); - found = true; - } - if (!found) - { - if (file) - fprintf (file, " %s", cgraph_node_name (node)); - node->address_taken = false; - changed = true; - if (cgraph_local_node_p (node)) - { - node->local.local = true; - if (file) - fprintf (file, " (local)"); - } - } - } - #ifdef ENABLE_CHECKING verify_cgraph (); #endif @@ -487,64 +315,6 @@ cgraph_remove_unreachable_nodes (bool before_inlining_p, FILE *file) return changed; } -/* Discover variables that have no longer address taken or that are read only - and update their flags. - - FIXME: This can not be done in between gimplify and omp_expand since - readonly flag plays role on what is shared and what is not. Currently we do - this transformation as part of ipa-reference pass, but it would make sense - to do it before early optimizations. */ - -void -ipa_discover_readonly_nonaddressable_vars (void) -{ - struct varpool_node *vnode; - if (dump_file) - fprintf (dump_file, "Clearing variable flags:"); - for (vnode = varpool_nodes; vnode; vnode = vnode->next) - if (vnode->finalized && varpool_all_refs_explicit_p (vnode) - && (TREE_ADDRESSABLE (vnode->decl) || !TREE_READONLY (vnode->decl))) - { - bool written = false; - bool address_taken = false; - int i; - struct ipa_ref *ref; - for (i = 0; ipa_ref_list_refering_iterate (&vnode->ref_list, i, ref) - && (!written || !address_taken); i++) - switch (ref->use) - { - case IPA_REF_ADDR: - address_taken = true; - break; - case IPA_REF_LOAD: - break; - case IPA_REF_STORE: - written = true; - break; - } - if (TREE_ADDRESSABLE (vnode->decl) && !address_taken) - { - if (dump_file) - fprintf (dump_file, " %s (addressable)", varpool_node_name (vnode)); - TREE_ADDRESSABLE (vnode->decl) = 0; - } - if (!TREE_READONLY (vnode->decl) && !address_taken && !written - /* Making variable in explicit section readonly can cause section - type conflict. - See e.g. gcc.c-torture/compile/pr23237.c */ - && DECL_SECTION_NAME (vnode->decl) == NULL) - { - if (dump_file) - fprintf (dump_file, " %s (read-only)", varpool_node_name (vnode)); - TREE_READONLY (vnode->decl) = 1; - } - } - if (dump_file) - fprintf (dump_file, "\n"); -} - -/* Return true when function NODE should be considered externally visible. */ - static bool cgraph_externally_visible_p (struct cgraph_node *node, bool whole_program) { @@ -654,11 +424,8 @@ function_and_variable_visibility (bool whole_program) if (!node->local.externally_visible && node->analyzed && !DECL_EXTERNAL (node->decl)) { - struct cgraph_node *alias; gcc_assert (whole_program || !TREE_PUBLIC (node->decl)); cgraph_make_decl_local (node->decl); - for (alias = node->same_body; alias; alias = alias->next) - cgraph_make_decl_local (alias->decl); if (node->same_comdat_group) /* cgraph_externally_visible_p has already checked all other nodes in the group and they will all be made local. We need to @@ -666,7 +433,10 @@ function_and_variable_visibility (bool whole_program) segfault though. */ dissolve_same_comdat_group_list (node); } - node->local.local = cgraph_local_node_p (node); + node->local.local = (cgraph_only_called_directly_p (node) + && node->analyzed + && !DECL_EXTERNAL (node->decl) + && !node->local.externally_visible); } for (vnode = varpool_nodes; vnode; vnode = vnode->next) { @@ -767,8 +537,7 @@ struct simple_ipa_opt_pass pass_ipa_function_and_variable_visibility = 0, /* properties_provided */ 0, /* properties_destroyed */ 0, /* todo_flags_start */ - TODO_remove_functions | TODO_dump_cgraph - | TODO_ggc_collect /* todo_flags_finish */ + TODO_remove_functions | TODO_dump_cgraph/* todo_flags_finish */ } }; @@ -823,8 +592,7 @@ struct ipa_opt_pass_d pass_ipa_whole_program_visibility = 0, /* properties_provided */ 0, /* properties_destroyed */ 0, /* todo_flags_start */ - TODO_remove_functions | TODO_dump_cgraph - | TODO_ggc_collect /* todo_flags_finish */ + TODO_dump_cgraph | TODO_remove_functions/* todo_flags_finish */ }, NULL, /* generate_summary */ NULL, /* write_summary */ @@ -994,241 +762,3 @@ debug_cgraph_node_set (cgraph_node_set set) dump_cgraph_node_set (stderr, set); } -/* Hash a varpool node set element. */ - -static hashval_t -hash_varpool_node_set_element (const void *p) -{ - const_varpool_node_set_element element = (const_varpool_node_set_element) p; - return htab_hash_pointer (element->node); -} - -/* Compare two varpool node set elements. */ - -static int -eq_varpool_node_set_element (const void *p1, const void *p2) -{ - const_varpool_node_set_element e1 = (const_varpool_node_set_element) p1; - const_varpool_node_set_element e2 = (const_varpool_node_set_element) p2; - - return e1->node == e2->node; -} - -/* Create a new varpool node set. */ - -varpool_node_set -varpool_node_set_new (void) -{ - varpool_node_set new_node_set; - - new_node_set = GGC_NEW (struct varpool_node_set_def); - new_node_set->hashtab = htab_create_ggc (10, - hash_varpool_node_set_element, - eq_varpool_node_set_element, - NULL); - new_node_set->nodes = NULL; - return new_node_set; -} - -/* Add varpool_node NODE to varpool_node_set SET. */ - -void -varpool_node_set_add (varpool_node_set set, struct varpool_node *node) -{ - void **slot; - varpool_node_set_element element; - struct varpool_node_set_element_def dummy; - - dummy.node = node; - slot = htab_find_slot (set->hashtab, &dummy, INSERT); - - if (*slot != HTAB_EMPTY_ENTRY) - { - element = (varpool_node_set_element) *slot; - gcc_assert (node == element->node - && (VEC_index (varpool_node_ptr, set->nodes, element->index) - == node)); - return; - } - - /* Insert node into hash table. */ - element = - (varpool_node_set_element) GGC_NEW (struct varpool_node_set_element_def); - element->node = node; - element->index = VEC_length (varpool_node_ptr, set->nodes); - *slot = element; - - /* Insert into node vector. */ - VEC_safe_push (varpool_node_ptr, gc, set->nodes, node); -} - -/* Remove varpool_node NODE from varpool_node_set SET. */ - -void -varpool_node_set_remove (varpool_node_set set, struct varpool_node *node) -{ - void **slot, **last_slot; - varpool_node_set_element element, last_element; - struct varpool_node *last_node; - struct varpool_node_set_element_def dummy; - - dummy.node = node; - slot = htab_find_slot (set->hashtab, &dummy, NO_INSERT); - if (slot == NULL) - return; - - element = (varpool_node_set_element) *slot; - gcc_assert (VEC_index (varpool_node_ptr, set->nodes, element->index) - == node); - - /* Remove from vector. We do this by swapping node with the last element - of the vector. */ - last_node = VEC_pop (varpool_node_ptr, set->nodes); - if (last_node != node) - { - dummy.node = last_node; - last_slot = htab_find_slot (set->hashtab, &dummy, NO_INSERT); - last_element = (varpool_node_set_element) *last_slot; - gcc_assert (last_element); - - /* Move the last element to the original spot of NODE. */ - last_element->index = element->index; - VEC_replace (varpool_node_ptr, set->nodes, last_element->index, - last_node); - } - - /* Remove element from hash table. */ - htab_clear_slot (set->hashtab, slot); - ggc_free (element); -} - -/* Find NODE in SET and return an iterator to it if found. A null iterator - is returned if NODE is not in SET. */ - -varpool_node_set_iterator -varpool_node_set_find (varpool_node_set set, struct varpool_node *node) -{ - void **slot; - struct varpool_node_set_element_def dummy; - varpool_node_set_element element; - varpool_node_set_iterator vsi; - - dummy.node = node; - slot = htab_find_slot (set->hashtab, &dummy, NO_INSERT); - if (slot == NULL) - vsi.index = (unsigned) ~0; - else - { - element = (varpool_node_set_element) *slot; - gcc_assert (VEC_index (varpool_node_ptr, set->nodes, element->index) - == node); - vsi.index = element->index; - } - vsi.set = set; - - return vsi; -} - -/* Dump content of SET to file F. */ - -void -dump_varpool_node_set (FILE *f, varpool_node_set set) -{ - varpool_node_set_iterator iter; - - for (iter = vsi_start (set); !vsi_end_p (iter); vsi_next (&iter)) - { - struct varpool_node *node = vsi_node (iter); - dump_varpool_node (f, node); - } -} - -/* Dump content of SET to stderr. */ - -void -debug_varpool_node_set (varpool_node_set set) -{ - dump_varpool_node_set (stderr, set); -} - - -/* Simple ipa profile pass propagating frequencies across the callgraph. */ - -static unsigned int -ipa_profile (void) -{ - struct cgraph_node **order = XCNEWVEC (struct cgraph_node *, cgraph_n_nodes); - struct cgraph_edge *e; - int order_pos; - bool something_changed = false; - int i; - - order_pos = cgraph_postorder (order); - for (i = order_pos - 1; i >= 0; i--) - { - if (order[i]->local.local && cgraph_propagate_frequency (order[i])) - { - for (e = order[i]->callees; e; e = e->next_callee) - if (e->callee->local.local && !e->callee->aux) - { - something_changed = true; - e->callee->aux = (void *)1; - } - } - order[i]->aux = NULL; - } - - while (something_changed) - { - something_changed = false; - for (i = order_pos - 1; i >= 0; i--) - { - if (order[i]->aux && cgraph_propagate_frequency (order[i])) - { - for (e = order[i]->callees; e; e = e->next_callee) - if (e->callee->local.local && !e->callee->aux) - { - something_changed = true; - e->callee->aux = (void *)1; - } - } - order[i]->aux = NULL; - } - } - free (order); - return 0; -} - -static bool -gate_ipa_profile (void) -{ - return flag_ipa_profile; -} - -struct ipa_opt_pass_d pass_ipa_profile = -{ - { - IPA_PASS, - "ipa-profile", /* name */ - gate_ipa_profile, /* gate */ - ipa_profile, /* execute */ - NULL, /* sub */ - NULL, /* next */ - 0, /* static_pass_number */ - TV_IPA_PROFILE, /* tv_id */ - 0, /* properties_required */ - 0, /* properties_provided */ - 0, /* properties_destroyed */ - 0, /* todo_flags_start */ - 0 /* todo_flags_finish */ - }, - NULL, /* generate_summary */ - NULL, /* write_summary */ - NULL, /* read_summary */ - NULL, /* write_optimization_summary */ - NULL, /* read_optimization_summary */ - NULL, /* stmt_fixup */ - 0, /* TODOs */ - NULL, /* function_transform */ - NULL /* variable_transform */ -}; diff --git a/gcc/testsuite/ChangeLog b/gcc/testsuite/ChangeLog index 937537d8d4b..15fb90b0358 100644 --- a/gcc/testsuite/ChangeLog +++ b/gcc/testsuite/ChangeLog @@ -1,3 +1,8 @@ +2010-04-27 Martin Jambor + + PR middle-end/43812 + * g++.dg/ipa/pr43812.C: New test. + 2010-04-27 Jan Hubicka * gcc.dg/ipa/iinline-1.c (main): Rename to... -- 2.11.0